diff --git a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/Trackable.kt b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/Trackable.kt index 326604374..0b87a5846 100644 --- a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/Trackable.kt +++ b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/Trackable.kt @@ -13,37 +13,33 @@ import org.hl7.elm.r1.Element * In Java use Trackable.INSTANCE to access the same functionality. */ object Trackable { - private fun Any.refHash(): Int = System.identityHashCode(this) - - private val trackerIdsByElement = HashMap() + private val trackerIdsByElement = WeakIdentityHashMap() var Element.trackerId: UUID - get() = trackerIdsByElement.getOrPut(this.refHash()) { UUID.randomUUID() } + get() = trackerIdsByElement.getOrPut(this) { UUID.randomUUID() } set(value) { - trackerIdsByElement[this.refHash()] = value + trackerIdsByElement[this] = value } - private val trackbacksByElement = HashMap>() + private val trackbacksByElement = WeakIdentityHashMap>() var Element.trackbacks: MutableList - get() = trackbacksByElement.getOrPut(this.refHash()) { ArrayList() } + get() = trackbacksByElement.getOrPut(this) { ArrayList() } set(value) { - trackbacksByElement[this.refHash()] = value + trackbacksByElement[this] = value } - private val resultTypesByElement = HashMap() + private val resultTypesByElement = WeakIdentityHashMap() var Element.resultType: DataType? - get() = resultTypesByElement[this.refHash()] + get() = resultTypesByElement[this] set(value) { - resultTypesByElement[this.refHash()] = value + if (value == null) { + resultTypesByElement.remove(this) + } else { + resultTypesByElement[this] = value + } } fun T.withResultType(resultType: DataType?): T { this.resultType = resultType return this } - - fun clear() { - trackerIdsByElement.clear() - trackbacksByElement.clear() - resultTypesByElement.clear() - } } diff --git a/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/WeakIdentityHashMap.kt b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/WeakIdentityHashMap.kt new file mode 100644 index 000000000..f08bb2704 --- /dev/null +++ b/Src/java/cql-to-elm/src/main/java/org/cqframework/cql/cql2elm/tracking/WeakIdentityHashMap.kt @@ -0,0 +1,70 @@ +package org.cqframework.cql.cql2elm.tracking + +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap + +const val DEFAULT_CLEANUP_THRESHOLD = 1000 + +/** + * This is a map that uses weak references for keys. This means that if the key is no longer + * strongly referenced anywhere in the program, it will be garbage collected and the entry will be + * removed from the map. This is useful for extension properties. The lifetime of the property is + * tied to the lifetime of the object + */ +internal class WeakIdentityHashMap { + + private val backingMap = ConcurrentHashMap, V>() + private var operationCount = 0 + private val cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD + + val size: Int + get() = backingMap.size + + operator fun get(key: K): V? { + incrementAndCleanUp() + return backingMap[WeakKey(key)] + } + + operator fun set(key: K, value: V) { + incrementAndCleanUp() + backingMap[WeakKey(key)] = value + } + + fun remove(key: K): V? { + incrementAndCleanUp() + return backingMap.remove(WeakKey(key)) + } + + fun getOrPut(key: K, defaultValue: () -> V): V { + incrementAndCleanUp() + return backingMap.getOrPut(WeakKey(key), defaultValue) + } + + private fun incrementAndCleanUp() { + operationCount++ + if (operationCount >= cleanupThreshold) { + cleanUp() + operationCount = 0 + } + } + + private fun cleanUp() { + val keys = backingMap.keys.filter { it.get() == null } + keys.forEach { backingMap.remove(it) } + } + + private class WeakKey(key: K) { + private val ref = WeakReference(key) + private val hashCode = System.identityHashCode(key) + + fun get(): K? = ref.get() + + override fun equals(other: Any?): Boolean { + if (other !is WeakKey<*>) return false + if (this.hashCode != other.hashCode) return false + return this.ref.get() === other.ref.get() + } + + override fun hashCode(): Int = hashCode + } +} diff --git a/Src/java/engine-fhir/src/test/java/org/hl7/fhirpath/TestFhirPath.java b/Src/java/engine-fhir/src/test/java/org/hl7/fhirpath/TestFhirPath.java index f7e1fc2d5..1da32c586 100644 --- a/Src/java/engine-fhir/src/test/java/org/hl7/fhirpath/TestFhirPath.java +++ b/Src/java/engine-fhir/src/test/java/org/hl7/fhirpath/TestFhirPath.java @@ -10,7 +10,6 @@ import java.time.Instant; import java.time.ZoneOffset; import java.util.*; -import org.cqframework.cql.cql2elm.tracking.Trackable; import org.fhir.ucum.UcumException; import org.hl7.elm.r1.Library; import org.hl7.elm.r1.VersionedIdentifier; @@ -122,7 +121,6 @@ protected void runTest( CompositeDataProvider provider, FhirModelResolver resolver) throws UcumException { - Trackable.INSTANCE.clear(); String cql = null; IBaseResource resource = null; if (test.getInputfile() != null) {