Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
Merge pull request #59 from atlanhq/DX-266
Browse files Browse the repository at this point in the history
Handle assigned terms serde in human-readable way
  • Loading branch information
cmgrote authored Oct 31, 2023
2 parents 3d91048 + efc6cda commit 430a0a2
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fun termsForDuplicates(glossaryQN: String, batchSize: Int) {
val keys = hashToAssetKeys[hash]
if (keys?.size!! > 1) {
val columns = hashToColumns[hash]
val batch = AssetBatch(Atlan.getDefaultClient(), "asset", batchSize, false, AssetBatch.CustomMetadataHandling.MERGE, true)
val batch = AssetBatch(Atlan.getDefaultClient(), batchSize, false, AssetBatch.CustomMetadataHandling.MERGE, true)
val termName = "Dup. ($hash)"
val term = try {
GlossaryTerm.findByNameFast(termName, glossaryQN)
Expand Down
2 changes: 1 addition & 1 deletion packages/migration-assistant-export/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
val jarPath = "$rootDir/jars"
val jarFile = "migration-assistant-$version.jar"
val jarFile = "migration-assistant-export-$version.jar"

plugins {
id("atlan-kotlin-sample")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.atlan.Atlan
import com.atlan.model.assets.Asset
import com.atlan.model.assets.AtlanQuery
import com.atlan.model.assets.AuthPolicy
import com.atlan.model.assets.GlossaryTerm
import com.atlan.model.assets.IAccessControl
import com.atlan.model.assets.INamespace
import com.atlan.model.assets.Link
Expand Down Expand Up @@ -120,9 +121,11 @@ class Exporter(private val config: Map<String, String>) : RowGenerator {

private fun getRelatedAttributesToExtract(): MutableList<AtlanField> {
return mutableListOf(
Asset.QUALIFIED_NAME, // for asset referencing
Asset.NAME, // for Link embedding
Asset.DESCRIPTION, // for README embedding
Link.LINK, // for Link embedding
GlossaryTerm.ANCHOR, // for assigned term containment
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/migration-assistant-import/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
val jarPath = "$rootDir/jars"
val jarFile = "migration-assistant-$version.jar"
val jarFile = "migration-assistant-import-$version.jar"

plugins {
id("atlan-kotlin-sample")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fun loadOpenAPISpec(connectionQN: String, spec: OpenAPISpecReader, batchSize: In
logger.error("Unable to save the APISpec.", e)
exitProcess(5)
}
val batch = AssetBatch(Atlan.getDefaultClient(), APIPath.TYPE_NAME, batchSize, false, AssetBatch.CustomMetadataHandling.MERGE, true)
val batch = AssetBatch(Atlan.getDefaultClient(), batchSize, false, AssetBatch.CustomMetadataHandling.MERGE, true)
val totalCount = spec.paths?.size!!.toLong()
if (totalCount > 0) {
logger.info("Creating an APIPath for each path defined within the spec (total: {})", totalCount)
Expand Down
90 changes: 90 additions & 0 deletions serde/src/main/kotlin/cache/AssetCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright 2023 Atlan Pte. Ltd. */
package cache

import com.atlan.model.assets.Asset
import java.util.concurrent.ConcurrentHashMap

/**
* Utility class for lazy-loading a cache of assets based on some human-constructable identity.
*/
abstract class AssetCache {
private val byIdentity: MutableMap<String, String?> = ConcurrentHashMap()
private val byGuid: MutableMap<String, Asset?> = ConcurrentHashMap()

/**
* Retrieve an asset from the cache by its human-readable identity, lazily-loading it on any cache misses.
*
* @param identity of the asset to retrieve
* @return the asset with the specified identity
*/
fun getByIdentity(identity: String): Asset? {
if (!this.containsIdentity(identity)) {
val asset = lookupAssetByIdentity(identity)!!
byIdentity[identity] = asset.guid
byGuid[asset.guid] = asset
}
return byGuid[byIdentity[identity]]
}

/**
* Retrieve an asset from the cache by its globally-unique identifier, lazily-loading it on any cache misses.
*
* @param guid unique identifier (GUID) of the asset to retrieve
* @return the asset with the specified GUID
*/
fun getByGuid(guid: String): Asset? {
if (!this.containsGuid(guid)) {
val asset = lookupAssetByGuid(guid)!!
byIdentity[getIdentityForAsset(asset)] = guid
byGuid[guid] = asset
}
return byGuid[guid]
}

/**
* Indicates whether the cache already contains an asset with a given identity.
*
* @param identity of the asset to check for presence in the cache
* @return true if this identity is already in the cache, false otherwise
*/
fun containsIdentity(identity: String): Boolean {
return byIdentity.containsKey(identity)
}

/**
* Indicates whether the cache already contains an asset with a given GUID.
*
* @param guid unique identifier (GUID) of the asset to check for presence in the cache
* @return true if this GUID is already in the cache, false otherwise
*/
fun containsGuid(guid: String): Boolean {
return byGuid.containsKey(guid)
}

/**
* Actually go to Atlan and find the asset with the provided identity.
* Note: this should also populate the byGuid cache
*
* @param identity of the asset to lookup
* @return the asset, from Atlan
*/
protected abstract fun lookupAssetByIdentity(identity: String?): Asset?

/**
* Actually go to Atlan and find the asset with the provided GUID.
* Note: this should also populate the byIdentity cache
*
* @param guid unique identifier (GUID) of the asset to lookup
* @return the asset, from Atlan
*/
protected abstract fun lookupAssetByGuid(guid: String?): Asset?

/**
* Create a unique, reconstructable identity for the provided asset.
*
* @param asset for which to construct the identity
* @return the identity of the asset
*/
protected abstract fun getIdentityForAsset(asset: Asset): String
}
46 changes: 46 additions & 0 deletions serde/src/main/kotlin/cache/GlossaryCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright 2023 Atlan Pte. Ltd. */
package cache

import com.atlan.exception.AtlanException
import com.atlan.model.assets.Asset
import com.atlan.model.assets.Glossary
import mu.KotlinLogging

object GlossaryCache : AssetCache() {

private val logger = KotlinLogging.logger {}

/** {@inheritDoc} */
override fun lookupAssetByIdentity(identity: String?): Asset? {
try {
return Glossary.findByName(identity)
} catch (e: AtlanException) {
logger.error("Unable to lookup or find glossary: {}", identity, e)
}
return null
}

/** {@inheritDoc} */
override fun lookupAssetByGuid(guid: String?): Asset? {
try {
val glossary = Glossary.select()
.where(Glossary.GUID.eq(guid))
.includeOnResults(Glossary.NAME)
.pageSize(2)
.stream()
.findFirst()
if (glossary.isPresent) {
return glossary.get()
}
} catch (e: AtlanException) {
logger.error("Unable to lookup or find glossary: {}", guid, e)
}
return null
}

/** {@inheritDoc} */
override fun getIdentityForAsset(asset: Asset): String {
return asset.name
}
}
80 changes: 80 additions & 0 deletions serde/src/main/kotlin/cache/TermCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright 2023 Atlan Pte. Ltd. */
package cache

import com.atlan.exception.AtlanException
import com.atlan.model.assets.Asset
import com.atlan.model.assets.Glossary
import com.atlan.model.assets.GlossaryTerm
import com.atlan.model.fields.AtlanField
import mu.KotlinLogging
import xformers.cell.AssignedTermXformer

object TermCache : AssetCache() {

private val logger = KotlinLogging.logger {}

private val includesOnResults: List<AtlanField> = listOf(GlossaryTerm.NAME, GlossaryTerm.ANCHOR)
private val includesOnRelations: List<AtlanField> = listOf(Glossary.NAME)

/** {@inheritDoc} */
override fun lookupAssetByIdentity(identity: String?): Asset? {
val tokens = identity?.split(AssignedTermXformer.TERM_GLOSSARY_DELIMITER)
if (tokens?.size == 2) {
val termName = tokens[0]
val glossaryName = tokens[1]
val glossary = GlossaryCache.getByIdentity(glossaryName)
if (glossary != null) {
try {
val term = GlossaryTerm.select()
.where(GlossaryTerm.NAME.eq(termName))
.where(GlossaryTerm.ANCHOR.eq(glossary.qualifiedName))
.includesOnResults(includesOnResults)
.includesOnRelations(includesOnRelations)
.pageSize(2)
.stream()
.findFirst()
if (term.isPresent) {
return term.get()
}
} catch (e: AtlanException) {
logger.error("Unable to lookup or find term: {}", identity, e)
}
} else {
logger.error("Unable to find glossary {} for term reference: {}", glossaryName, identity)
}
} else {
logger.error("Unable to lookup or find term, unexpected reference: {}", identity)
}
return null
}

/** {@inheritDoc} */
override fun lookupAssetByGuid(guid: String?): Asset? {
try {
val term = GlossaryTerm.select()
.where(GlossaryTerm.GUID.eq(guid))
.includesOnResults(includesOnResults)
.includesOnRelations(includesOnRelations)
.pageSize(2)
.stream()
.findFirst()
if (term.isPresent) {
return term.get()
}
} catch (e: AtlanException) {
logger.error("Unable to lookup or find term: {}", guid, e)
}
return null
}

/** {@inheritDoc} */
override fun getIdentityForAsset(asset: Asset): String {
return when (asset) {
is GlossaryTerm -> {
"${asset.name}${AssignedTermXformer.TERM_GLOSSARY_DELIMITER}${asset.anchor.name}"
}
else -> ""
}
}
}
3 changes: 3 additions & 0 deletions serde/src/main/kotlin/xformers/cell/AssetRefXformer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package xformers.cell

import com.atlan.Atlan
import com.atlan.model.assets.Asset
import com.atlan.model.assets.GlossaryTerm
import com.atlan.model.assets.Link
import com.atlan.model.assets.Readme
import com.atlan.serde.Serde
Expand Down Expand Up @@ -33,6 +34,7 @@ object AssetRefXformer {
.build()
.toJson(Atlan.getDefaultClient())
}
is GlossaryTerm -> AssignedTermXformer.encode(asset)
else -> {
var qualifiedName = asset.qualifiedName
if (asset.qualifiedName.isNullOrEmpty() && asset.uniqueAttributes != null) {
Expand All @@ -54,6 +56,7 @@ object AssetRefXformer {
return when (fieldName) {
"readme" -> Readme._internal().description(assetRef).build()
"links" -> Atlan.getDefaultClient().readValue(assetRef, Link::class.java)
"meanings" -> AssignedTermXformer.decode(assetRef, fieldName)
else -> {
val tokens = assetRef.split(TYPE_QN_DELIMITER)
val typeName = tokens[0]
Expand Down
56 changes: 56 additions & 0 deletions serde/src/main/kotlin/xformers/cell/AssignedTermXformer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright 2023 Atlan Pte. Ltd. */
package xformers.cell

import cache.TermCache
import com.atlan.model.assets.Asset
import com.atlan.model.assets.GlossaryTerm
import mu.KotlinLogging

/**
* Static object to transform term assignment references.
*/
object AssignedTermXformer {

private val logger = KotlinLogging.logger {}

const val TERM_GLOSSARY_DELIMITER = "@@@"

/**
* Encodes (serializes) a term assignment into a string form.
*
* @param asset to be encoded
* @return the string-encoded form for that asset
*/
fun encode(asset: Asset): String {
// Handle some assets as direct embeds
return when (asset) {
is GlossaryTerm -> {
val term = TermCache.getByGuid(asset.guid)
if (term is GlossaryTerm) {
"${term.name}$TERM_GLOSSARY_DELIMITER${term.anchor.name}"
} else {
logger.error("Unable to find any term with GUID: {}", asset.guid)
""
}
}
else -> AssetRefXformer.encode(asset)
}
}

/**
* Decodes (deserializes) a string form into a term assignment object.
*
* @param assetRef the string form to be decoded
* @param fieldName the name of the field containing the string-encoded value
* @return the term assignment represented by the string
*/
fun decode(assetRef: String, fieldName: String): Asset {
return when (fieldName) {
"meanings" -> {
TermCache.getByIdentity(assetRef)?.trimToReference()!!
}
else -> AssetRefXformer.decode(assetRef, fieldName)
}
}
}

0 comments on commit 430a0a2

Please sign in to comment.