diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessor.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessor.java new file mode 100644 index 0000000000..a024d79af7 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessor.java @@ -0,0 +1,285 @@ +package org.opencds.cqf.fhir.cr.hapi.common; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.parser.path.EncodeContextPath; +import ca.uhn.fhir.parser.path.EncodeContextPathElement; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.PlanDefinition; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ValueSet; +import org.opencds.cqf.fhir.cr.common.ArtifactDiffProcessor.DiffCache; +import org.opencds.cqf.fhir.cr.common.ArtifactDiffProcessor.DiffCache.DiffCacheResource; +import org.opencds.cqf.fhir.cr.common.CreateChangelogProcessor.ChangeLog; +import org.opencds.cqf.fhir.cr.common.ICreateChangelogProcessor; +import org.opencds.cqf.fhir.cr.common.PackageProcessor; +import org.opencds.cqf.fhir.cr.crmi.KnowledgeArtifactProcessor; +import org.opencds.cqf.fhir.utility.Canonicals; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.springframework.beans.BeanWrapperImpl; + +@SuppressWarnings("UnstableApiUsage") +public class HapiCreateChangelogProcessor implements ICreateChangelogProcessor { + + private final FhirVersionEnum fhirVersion; + private final PackageProcessor packageProcessor; + + private final HapiArtifactDiffProcessor hapiArtifactDiffProcessor; + + public HapiCreateChangelogProcessor(IRepository repository) { + this.fhirVersion = repository.fhirContext().getVersion().getVersion(); + this.packageProcessor = new PackageProcessor(repository); + this.hapiArtifactDiffProcessor = new HapiArtifactDiffProcessor(repository); + } + + @Override + public IBaseResource createChangelog( + IBaseResource source, IBaseResource target, IBaseResource terminologyEndpoint) { + + // 1) Use package to get a pair of bundles + ExecutorService service = Executors.newCachedThreadPool(); + List> packages; + Bundle sourceBundle; + Bundle targetBundle; + Parameters params = new Parameters(); + params.addParameter().setName("terminologyEndpoint").setResource((Resource) terminologyEndpoint); + try { + packages = service.invokeAll(Arrays.asList( + () -> packageProcessor.packageResource(source, params), + () -> packageProcessor.packageResource(target, params))); + sourceBundle = (Bundle) packages.get(0).get(); + targetBundle = (Bundle) packages.get(1).get(); + service.shutdownNow(); + } catch (InterruptedException | ExecutionException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new UnprocessableEntityException(e.getMessage()); + } finally { + service.shutdown(); + } + + // 2) Fill the cache with the bundle contents + var cache = populateCache(source, sourceBundle, target, targetBundle); + + // 3) Use cached resources to create diff and changelog + var targetResource = cache.getTargetResourceForUrl(((MetadataResource) target).getUrl()); + var sourceResource = cache.getSourceResourceForUrl(((MetadataResource) source).getUrl()); + if (targetResource.isPresent() && sourceResource.isPresent()) { + var targetAdapter = IAdapterFactory.forFhirVersion(FhirVersionEnum.R4) + .createKnowledgeArtifactAdapter(targetResource.get().resource); + var diffParameters = hapiArtifactDiffProcessor.getArtifactDiff( + sourceResource.get().resource, + targetResource.get().resource, + true, + true, + cache, + terminologyEndpoint); + var manifestUrl = targetAdapter.getUrl(); + var changelog = new ChangeLog(manifestUrl); + processChanges(((Parameters) diffParameters).getParameter(), changelog, cache, manifestUrl); + + // 4) Handle the Conditions and Priorities which are in RelatedArtifact changes + changelog.handleRelatedArtifacts(); + + // 5) Generate the output JSON + var bin = new Binary(); + var mapper = createSerializer(); + try { + bin.setContent(mapper.writeValueAsString(changelog).getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + throw new UnprocessableEntityException(e.getMessage()); + } + + return bin; + } + + return null; + } + + private DiffCache populateCache( + IBaseResource source, Bundle sourceBundle, IBaseResource target, Bundle targetBundle) { + var cache = new DiffCache(); + for (final var entry : sourceBundle.getEntry()) { + if (entry.hasResource() && entry.getResource() instanceof MetadataResource metadataResource) { + cache.addSource(metadataResource.getUrl() + "|" + metadataResource.getVersion(), metadataResource); + if (metadataResource.getIdPart().equals(source.getIdElement().getIdPart())) { + cache.addSource(metadataResource.getUrl(), metadataResource); + } + } + } + for (final var entry : targetBundle.getEntry()) { + if (entry.hasResource() && entry.getResource() instanceof MetadataResource metadataResource) { + cache.addTarget(metadataResource.getUrl() + "|" + metadataResource.getVersion(), metadataResource); + if (metadataResource.getIdPart().equals(target.getIdElement().getIdPart())) { + cache.addTarget(metadataResource.getUrl(), metadataResource); + } + } + } + return cache; + } + + private ObjectMapper createSerializer() { + var mapper = new ObjectMapper() + .setDefaultPropertyInclusion(Include.NON_NULL) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + SimpleModule module = new SimpleModule("IBaseSerializer", new Version(1, 0, 0, null, null, null)); + module.addSerializer(IBase.class, new IBaseSerializer(FhirContext.forVersion(this.fhirVersion))); + mapper.registerModule(module); + return mapper; + } + + private void processChanges( + List changes, ChangeLog changelog, DiffCache cache, String url) { + // 1) Get the source and target resources so we can pull additional info as necessary + var resourceType = Canonicals.getResourceType(url); + // Check if the resource pair was already processed + var wasPageAlreadyProcessed = changelog.getPage(url).isPresent(); + if (!wasPageAlreadyProcessed + && (cache.getSourceResourceForUrl(url).isPresent() + || cache.getTargetResourceForUrl(url).isPresent())) { + final Optional sourceCacheResource = cache.getSourceResourceForUrl(url); + final Optional targetCacheResource = cache.getTargetResourceForUrl(url); + if (resourceType != null) { + MetadataResource sourceResource = sourceCacheResource + .map(diffCacheResource -> diffCacheResource.resource) + .orElse(null); + MetadataResource targetResource = targetCacheResource + .map(diffCacheResource -> diffCacheResource.resource) + .orElse(null); + // don't generate changeLog pages for non-grouper ValueSets + if (resourceType.equals("ValueSet") + && ((sourceResource != null && !KnowledgeArtifactProcessor.isGrouper(sourceResource)) + || (targetResource != null && !KnowledgeArtifactProcessor.isGrouper(targetResource)))) { + return; + } + // 2) Generate a page for each resource pair based on ResourceType + var page = changelog.getPage(url).orElseGet(() -> switch (resourceType) { + case "ValueSet" -> changelog.addPage((ValueSet) sourceResource, (ValueSet) targetResource, cache); + case "Library" -> changelog.addPage((Library) sourceResource, (Library) targetResource); + case "PlanDefinition" -> + changelog.addPage((PlanDefinition) sourceResource, (PlanDefinition) targetResource); + default -> changelog.addPage(sourceResource, targetResource, url); + }); + // 3) Process each change + for (var change : changes) { + processChange(changelog, cache, change, sourceResource, page); + } + } + } + } + + private void processChange( + ChangeLog changelog, + DiffCache cache, + ParametersParameterComponent change, + MetadataResource sourceResource, + ChangeLog.Page page) { + if (change.hasName() + && !change.getName().equals("operation") + && change.hasResource() + && change.getResource() instanceof Parameters parameters) { + // Nested Parameters objects get recursively processed + processChanges(parameters.getParameter(), changelog, cache, change.getName()); + } else if (change.getName().equals("operation")) { + // 1) For each operation get the relevant parameters + var type = getStringParameter(change, "type") + .orElseThrow(() -> new UnprocessableEntityException( + "Type must be provided when adding an operation to the ChangeLog")); + var newValue = getParameter(change, "value"); + var path = getPathParameterNoBase(change); + var originalValue = getParameter(change, "previousValue").map(o -> (Object) o); + // try to extract the original value from the + // source object if not present in the Diff + // Parameters object + try { + if (originalValue.isEmpty() && !type.equals("insert") && sourceResource != null && path.isPresent()) { + originalValue = Optional.of((new BeanWrapperImpl(sourceResource).getPropertyValue(path.get()))); + } + } catch (Exception e) { + throw new InternalErrorException("Could not process path: " + path + ": " + e.getMessage()); + } + + // 2) Add a new operation to the ChangeLog + page.addOperation(type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null)); + } + } + + private Optional getPathParameterNoBase(Parameters.ParametersParameterComponent change) { + return getStringParameter(change, "path").map(p -> { + var e = new EncodeContextPath(p); + return removeBase(e); + }); + } + + private String removeBase(EncodeContextPath path) { + return path.getPath().subList(1, path.getPath().size()).stream() + .map(EncodeContextPathElement::toString) + .collect(Collectors.joining(".")); + } + + private Optional getStringParameter(Parameters.ParametersParameterComponent part, String name) { + return part.getPart().stream() + .filter(p -> p.getName().equalsIgnoreCase(name)) + .filter(p -> p.getValue() instanceof IPrimitiveType) + .map(p -> (IPrimitiveType) p.getValue()) + .map(s -> (String) s.getValue()) + .findAny(); + } + + private Optional getParameter(Parameters.ParametersParameterComponent part, String name) { + return part.getPart().stream() + .filter(p -> p.getName().equalsIgnoreCase(name)) + .filter(ParametersParameterComponent::hasValue) + .map(p -> (IBase) p.getValue()) + .findAny(); + } + + public static class IBaseSerializer extends StdSerializer { + private final transient IParser parser; + + public IBaseSerializer(FhirContext fhirCtx) { + super(IBase.class); + parser = fhirCtx.newJsonParser().setPrettyPrint(true); + } + + @Override + public void serialize(IBase resource, JsonGenerator jsonGenerator, SerializerProvider provider) + throws IOException { + String resourceJson = parser.encodeToString(resource); + jsonGenerator.writeRawValue(resourceJson); + } + } +} diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java index ba13fa7a47..6eb1de7c44 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java @@ -9,6 +9,7 @@ import org.opencds.cqf.fhir.cr.graphdefinition.GraphDefinitionProcessor; import org.opencds.cqf.fhir.cr.graphdefinition.apply.ApplyRequestBuilder; import org.opencds.cqf.fhir.cr.hapi.common.HapiArtifactDiffProcessor; +import org.opencds.cqf.fhir.cr.hapi.common.HapiCreateChangelogProcessor; import org.opencds.cqf.fhir.cr.hapi.common.IActivityDefinitionProcessorFactory; import org.opencds.cqf.fhir.cr.hapi.common.ICqlProcessorFactory; import org.opencds.cqf.fhir.cr.hapi.common.IGraphDefinitionApplyRequestBuilderFactory; @@ -72,7 +73,10 @@ IQuestionnaireResponseProcessorFactory questionnaireResponseProcessorFactory( ILibraryProcessorFactory libraryProcessorFactory(IRepositoryFactory repositoryFactory, CrSettings crSettings) { return rd -> { var repository = repositoryFactory.create(rd); - return new LibraryProcessor(repository, crSettings, List.of(new HapiArtifactDiffProcessor(repository))); + return new LibraryProcessor( + repository, + crSettings, + List.of(new HapiArtifactDiffProcessor(repository), new HapiCreateChangelogProcessor(repository))); }; } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java index 0695e47b6f..b493b60dec 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java @@ -65,7 +65,8 @@ RetireOperationConfig.class, WithdrawOperationConfig.class, ReviseOperationConfig.class, - ArtifactDiffOperationConfig.class + ArtifactDiffOperationConfig.class, + CreateChangelogOperationConfig.class }) public class CrR4Config { diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CreateChangelogOperationConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CreateChangelogOperationConfig.java new file mode 100644 index 0000000000..36086aae01 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CreateChangelogOperationConfig.java @@ -0,0 +1,35 @@ +package org.opencds.cqf.fhir.cr.hapi.config.r4; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.RestfulServer; +import java.util.Arrays; +import java.util.Map; +import org.opencds.cqf.fhir.cr.hapi.common.ILibraryProcessorFactory; +import org.opencds.cqf.fhir.cr.hapi.config.CrProcessorConfig; +import org.opencds.cqf.fhir.cr.hapi.config.ProviderLoader; +import org.opencds.cqf.fhir.cr.hapi.config.ProviderSelector; +import org.opencds.cqf.fhir.cr.hapi.r4.library.LibraryCreateChangelogProvider; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(CrProcessorConfig.class) +public class CreateChangelogOperationConfig { + + @Bean + LibraryCreateChangelogProvider r4LibraryCreateChangelogProvider(ILibraryProcessorFactory libraryProcessorFactory) { + return new LibraryCreateChangelogProvider(libraryProcessorFactory); + } + + @Bean(name = "createChangelogOperationLoader") + public ProviderLoader createChangelogOperationLoader( + ApplicationContext applicationContext, FhirContext fhirContext, RestfulServer restfulServer) { + var selector = new ProviderSelector( + fhirContext, Map.of(FhirVersionEnum.R4, Arrays.asList(LibraryCreateChangelogProvider.class))); + + return new ProviderLoader(restfulServer, applicationContext, selector); + } +} diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/library/LibraryCreateChangelogProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/library/LibraryCreateChangelogProvider.java new file mode 100644 index 0000000000..bd61861976 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/library/LibraryCreateChangelogProvider.java @@ -0,0 +1,48 @@ +package org.opencds.cqf.fhir.cr.hapi.r4.library; + +import static org.opencds.cqf.fhir.cr.hapi.common.IdHelper.getIdType; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Endpoint; +import org.hl7.fhir.r4.model.Library; +import org.opencds.cqf.fhir.cr.hapi.common.ILibraryProcessorFactory; +import org.opencds.cqf.fhir.utility.monad.Eithers; + +public class LibraryCreateChangelogProvider { + + private final ILibraryProcessorFactory libraryProcessorFactory; + + private final FhirVersionEnum fhirVersion; + + public LibraryCreateChangelogProvider(ILibraryProcessorFactory libraryProcessorFactory) { + this.libraryProcessorFactory = libraryProcessorFactory; + this.fhirVersion = FhirVersionEnum.R4; + } + + @Operation(name = "$create-changelog", idempotent = true, global = true, type = Library.class) + @Description( + shortDefinition = "$create-changelog", + value = "Create a changelog object which can be easily rendered into a table") + public IBaseResource crmiArtifactDiff( + RequestDetails requestDetails, + @OperationParam(name = "source") String source, + @OperationParam(name = "target") String target, + @OperationParam(name = "terminologyEndpoint") Endpoint terminologyEndpoint) + throws UnprocessableEntityException, ResourceNotFoundException { + IIdType sourceId = getIdType(fhirVersion, "Library", source); + IIdType targetId = getIdType(fhirVersion, "Library", target); + + return libraryProcessorFactory + .create(requestDetails) + .createChangelog( + Eithers.for3(null, sourceId, null), Eithers.for3(null, targetId, null), terminologyEndpoint); + } +} diff --git a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessorTest.java b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessorTest.java new file mode 100644 index 0000000000..f5c40be47d --- /dev/null +++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessorTest.java @@ -0,0 +1,642 @@ +package org.opencds.cqf.fhir.cr.hapi.common; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.ClasspathUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.StreamSupport; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.Canonicals; +import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; +import org.springframework.data.util.StreamUtils; + +class HapiCreateChangelogProcessorTest { + + public HapiCreateChangelogProcessor createChangelogProcessor; + public InMemoryFhirRepository repository; + + @Test + void create_changelog_pages() { + var repository = new InMemoryFhirRepository(FhirContext.forR4()); + createChangelogProcessor = new HapiCreateChangelogProcessor(repository); + + Bundle sourceBundle = ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-diff-bundle.json"); + Bundle targetBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-dxtc-modified-diff-bundle.json"); + repository.transaction(sourceBundle); + repository.transaction(targetBundle); + Library source = sourceBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + Library target = targetBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + + // check that the correct pages are created + var returnedBinary = (Binary) createChangelogProcessor.createChangelog(source, target, null); + assertNotNull(returnedBinary); + byte[] decodedBytes = Base64.getDecoder().decode(returnedBinary.getContentAsBase64()); + String decodedString = new String(decodedBytes); + ObjectMapper mapper = new ObjectMapper(); + var pageURLS = List.of( + "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "http://ersd.aimsplatform.org/fhir/Library/rctc", + "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "http://snomed.info/sct"); + assertDoesNotThrow(() -> { + var node = mapper.readTree(decodedString); + assertTrue(node.get("pages").isArray()); + var pages = node.get("pages"); + assertEquals(pageURLS.size(), pages.size()); + for (final var url : pageURLS) { + var pageExists = StreamSupport.stream(pages.spliterator(), false) + .anyMatch(page -> page.get("url").asText().equals(url)); + assertTrue(pageExists); + } + }); + } + + @Test + void create_changelog_codes() { + // check that the correct leaf VS codes are generated and have + // the correct memberOID values + var repository = new InMemoryFhirRepository(FhirContext.forR4()); + createChangelogProcessor = new HapiCreateChangelogProcessor(repository); + + Bundle sourceBundle = ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-diff-bundle.json"); + Bundle targetBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-dxtc-modified-diff-bundle.json"); + repository.transaction(sourceBundle); + repository.transaction(targetBundle); + Library source = sourceBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + Library target = targetBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + + // check that the correct pages are created + var returnedBinary = (Binary) createChangelogProcessor.createChangelog(source, target, null); + assertNotNull(returnedBinary); + byte[] decodedBytes = Base64.getDecoder().decode(returnedBinary.getContentAsBase64()); + String decodedString = new String(decodedBytes); + ObjectMapper mapper = new ObjectMapper(); + Map oldCodes = new HashMap<>(); + oldCodes.put("772155008", new CodeAndOperation("2.16.840.1.113883.3.464.1003.113.11.1090", null)); + oldCodes.put("1086051000119107", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("1086061000119109", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("1086071000119103", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("1090211000119102", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("129667001", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("13596001", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("15682004", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("186347006", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("18901009", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("194945009", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("230596007", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("240422004", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("26117009", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("276197005", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("276197005", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("3419005", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("397428000", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("397430003", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("48278001", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("50215002", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("715659006", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("75589004", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("7773002", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("789005009", new CodeAndOperation("2.16.840.1.113762.1.4.1146.6", "delete")); + oldCodes.put("127631000119105", new CodeAndOperation("fake.oid.to.trigger.naive.expansion", null)); + oldCodes.put("15693281000119105", new CodeAndOperation("fake.oid.to.trigger.naive.expansion", "delete")); + var newCodes = new HashMap(); + newCodes.put("772155008", new CodeAndOperation("2.16.840.1.113883.3.464.1003.113.11.1090", null)); + newCodes.put("1193749009", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("1193750009", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("240349003", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("240350003", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("240351004", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("447282003", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("63650001", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("81020007", new CodeAndOperation("2.16.840.1.113762.1.4.1146.163", "insert")); + newCodes.put("127631000119105", new CodeAndOperation("fake.oid.to.trigger.naive.expansion", null)); + newCodes.put("15693201000119102", new CodeAndOperation("fake.oid.to.trigger.naive.expansion", "insert")); + newCodes.put("15693241000119100", new CodeAndOperation("fake.oid.to.trigger.naive.expansion", "insert")); + + assertDoesNotThrow(() -> { + var node = mapper.readTree(decodedString); + assertTrue(node.get("pages").isArray()); + var pages = node.get("pages"); + for (final var page : pages) { + if (Canonicals.getResourceType(page.get("url").asText()).equals("ValueSet")) { + assertTrue(page.get("oldData").get("codes").isArray()); + for (final var code : page.get("oldData").get("codes")) { + CodeAndOperation expectedOldCode = + oldCodes.get(code.get("codeValue").asText()); + assertNotNull(expectedOldCode); + if (expectedOldCode.operation != null) { + assertEquals( + expectedOldCode.operation, + code.get("operation").get("type").asText()); + assertEquals( + expectedOldCode.code, code.get("memberOid").asText()); + } + } + assertTrue(page.get("newData").get("codes").isArray()); + for (final var code : page.get("newData").get("codes")) { + CodeAndOperation expectedNewCode = + newCodes.get(code.get("codeValue").asText()); + assertNotNull(expectedNewCode); + if (expectedNewCode.operation != null) { + assertEquals( + expectedNewCode.operation, + code.get("operation").get("type").asText()); + assertEquals( + expectedNewCode.code, code.get("memberOid").asText()); + } + } + } + } + }); + } + + @Test + void create_changelog_conditions_and_priorities() { + // check that the conditions and priorities are correctly + // extracted and have the correct operations + var repository = new InMemoryFhirRepository(FhirContext.forR4()); + createChangelogProcessor = new HapiCreateChangelogProcessor(repository); + + Bundle sourceBundle = ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-diff-bundle.json"); + Bundle targetBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-dxtc-modified-diff-bundle.json"); + repository.transaction(sourceBundle); + repository.transaction(targetBundle); + Library source = sourceBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + Library target = targetBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + + var returnedBinary = (Binary) createChangelogProcessor.createChangelog(source, target, null); + assertNotNull(returnedBinary); + Map>> oldLeafsAndConditions = Map.of( + "2.16.840.1.113883.3.464.1003.113.11.1090", + Map.of( + "conditions", + List.of( + new CodeAndOperation("49649001", null), + new CodeAndOperation("000000000", "delete")), + "priority", List.of(new CodeAndOperation("routine", null))), + "2.16.840.1.113762.1.4.1146.6", + Map.of( + "conditions", + List.of( + new CodeAndOperation("49649001", null), + new CodeAndOperation("767146004", null)), + "priority", List.of(new CodeAndOperation("emergent", null))), + "2.16.840.1.113762.1.4.1146.1505", + Map.of( + "conditions", List.of(new CodeAndOperation("49649001", null)), + "priority", List.of(new CodeAndOperation("routine", null))), + "fake.oid.to.trigger.naive.expansion", + Map.of( + "conditions", List.of(new CodeAndOperation("49649001", null)), + "priority", List.of(new CodeAndOperation("routine", null)))); + Map>> newLeafsAndConditions = Map.of( + "2.16.840.1.113883.3.464.1003.113.11.1090", + Map.of( + "conditions", + List.of( + new CodeAndOperation("767146004", "insert"), + new CodeAndOperation("49649001", null)), + "priority", List.of(new CodeAndOperation("emergent", "replace"))), + "2.16.840.1.113762.1.4.1146.163", + Map.of( + "conditions", List.of(new CodeAndOperation("123123123", null)), + "priority", List.of(new CodeAndOperation("emergent", null))), + "2.16.840.1.113762.1.4.1146.1505", + Map.of( + "conditions", List.of(new CodeAndOperation("49649001", null)), + "priority", List.of(new CodeAndOperation("routine", null))), + "fake.oid.to.trigger.naive.expansion", + Map.of( + "conditions", List.of(new CodeAndOperation("49649001", null)), + "priority", List.of(new CodeAndOperation("routine", null)))); + ObjectMapper mapper = new ObjectMapper(); + assertDoesNotThrow(() -> { + var node = mapper.readTree(new String(Base64.getDecoder().decode(returnedBinary.getContentAsBase64()))); + assertTrue(node.get("pages").isArray()); + var pages = node.get("pages"); + for (final var page : pages) { + if (Canonicals.getResourceType(page.get("url").asText()).equals("ValueSet")) { + assertTrue(page.get("oldData").get("leafValueSets").isArray()); + assertTrue(page.get("oldData") + .get("priority") + .get("value") + .asText() + .equals("routine")); + for (final var leaf : page.get("oldData").get("leafValueSets")) { + assertTrue(leaf.get("conditions").isArray()); + var memberOid = leaf.get("memberOid").asText(); + assertTrue(oldLeafsAndConditions.containsKey(memberOid)); + List expectedConditions = + oldLeafsAndConditions.get(memberOid).get("conditions"); + assertTrue(expectedConditions.size() > 0); + for (final var condition : leaf.get("conditions")) { + Optional conditionInList = expectedConditions.stream() + .filter(c -> c.code != null + && c.code.equals( + condition.get("codeValue").asText())) + .findAny(); + assertTrue(conditionInList.isPresent()); + if (conditionInList.get().operation != null) { + assertEquals( + conditionInList.get().operation, + condition.get("operation").get("type").asText()); + } + } + assertNotNull(leaf.get("priority").get("value")); + CodeAndOperation expectedPriority = oldLeafsAndConditions + .get(memberOid) + .get("priority") + .get(0); + assertEquals( + expectedPriority.code, + leaf.get("priority").get("value").asText()); + if (expectedPriority.operation != null) { + assertEquals( + expectedPriority.operation, + leaf.get("priority") + .get("operation") + .get("type") + .asText()); + } + } + assertTrue(page.get("newData").get("leafValueSets").isArray()); + assertTrue(page.get("newData") + .get("priority") + .get("value") + .asText() + .equals("routine")); + for (final var leaf : page.get("newData").get("leafValueSets")) { + assertTrue(leaf.get("conditions").isArray()); + var memberOid = leaf.get("memberOid").asText(); + assertTrue(newLeafsAndConditions.containsKey(memberOid)); + List expectedConditions = + newLeafsAndConditions.get(memberOid).get("conditions"); + assertTrue(expectedConditions.size() > 0); + for (final var condition : leaf.get("conditions")) { + Optional conditionInList = expectedConditions.stream() + .filter(c -> c.code != null + && c.code.equals( + condition.get("codeValue").asText())) + .findAny(); + assertTrue(conditionInList.isPresent()); + if (conditionInList.get().operation != null) { + assertEquals( + conditionInList.get().operation, + condition.get("operation").get("type").asText()); + } + } + assertNotNull(leaf.get("priority").get("value")); + CodeAndOperation expectedPriority = newLeafsAndConditions + .get(memberOid) + .get("priority") + .get(0); + assertEquals( + expectedPriority.code, + leaf.get("priority").get("value").asText()); + if (expectedPriority.operation != null) { + assertEquals( + expectedPriority.operation, + leaf.get("priority") + .get("operation") + .get("type") + .asText()); + } + } + } + } + }); + } + + @Test + void create_changelog_grouped_leaf() { + // check that all the grouped leaf valuesets exist + var repository = new InMemoryFhirRepository(FhirContext.forR4()); + createChangelogProcessor = new HapiCreateChangelogProcessor(repository); + + Bundle sourceBundle = ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-diff-bundle.json"); + Bundle targetBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-dxtc-modified-diff-bundle.json"); + repository.transaction(sourceBundle); + repository.transaction(targetBundle); + Library source = sourceBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + Library target = targetBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + + var returnedBinary = (Binary) createChangelogProcessor.createChangelog(source, target, null); + assertNotNull(returnedBinary); + ObjectMapper mapper = new ObjectMapper(); + Exception expectNoException = null; + var oldLeafs = Map.of( + "2.16.840.1.113883.3.464.1003.113.11.1090", "", + "2.16.840.1.113762.1.4.1146.6", "delete", + "2.16.840.1.113762.1.4.1146.1505", "", + "fake.oid.to.trigger.naive.expansion", ""); + var newLeafs = Map.of( + "2.16.840.1.113883.3.464.1003.113.11.1090", "", + "2.16.840.1.113762.1.4.1146.163", "insert", + "2.16.840.1.113762.1.4.1146.1505", "", + "fake.oid.to.trigger.naive.expansion", ""); + assertDoesNotThrow(() -> { + var node = mapper.readTree(new String(Base64.getDecoder().decode(returnedBinary.getContentAsBase64()))); + assertTrue(node.get("pages").isArray()); + var pages = node.get("pages"); + for (final var page : pages) { + if (Canonicals.getResourceType(page.get("url").asText()).equals("ValueSet")) { + assertTrue(page.get("oldData").get("leafValueSets").isArray()); + for (final var leaf : page.get("oldData").get("leafValueSets")) { + var expectedLeaf = oldLeafs.get(leaf.get("memberOid").asText()); + assertNotNull(expectedLeaf); + if (!expectedLeaf.isBlank()) { + assertEquals( + expectedLeaf, + leaf.get("operation").get("type").asText()); + } + } + assertTrue(page.get("newData").get("leafValueSets").isArray()); + for (final var leaf : page.get("newData").get("leafValueSets")) { + var expectedLeaf = newLeafs.get(leaf.get("memberOid").asText()); + assertNotNull(expectedLeaf); + if (!expectedLeaf.isBlank()) { + assertEquals( + expectedLeaf, + leaf.get("operation").get("type").asText()); + } + } + } + } + }); + } + + @Test + void create_changelog_extracts_vs_name_and_url() { + var repository = new InMemoryFhirRepository(FhirContext.forR4()); + createChangelogProcessor = new HapiCreateChangelogProcessor(repository); + + Bundle sourceBundle = ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-diff-bundle.json"); + Bundle targetBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-dxtc-modified-diff-bundle.json"); + repository.transaction(sourceBundle); + repository.transaction(targetBundle); + Library source = sourceBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + Library target = targetBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + + var returnedBinary = (Binary) createChangelogProcessor.createChangelog(source, target, null); + assertNotNull(returnedBinary); + ObjectMapper mapper = new ObjectMapper(); + var oldLeafValueSetNames = List.of( + "Diagnosis_ProblemTriggersforPublicHealthReporting", + "DiphtheriaDisordersSNOMED", + "AnkylosingSpondylitis", + "AcanthamoebaDiseaseKeratitisDisordersSNOMED"); + var newLeafValueSetNames = List.of( + "Diagnosis_ProblemTriggersforPublicHealthReporting", + "AnkylosingSpondylitis", + "Cholera (Disorders) (SNOMED)", + "UpdatedName"); + assertDoesNotThrow(() -> { + var node = mapper.readTree(new String(Base64.getDecoder().decode(returnedBinary.getContentAsBase64()))); + assertTrue(node.get("pages").isArray()); + var pages = node.get("pages"); + for (final var page : pages) { + if (Canonicals.getResourceType(page.get("url").asText()).equals("ValueSet")) { + assertTrue(oldLeafValueSetNames.contains( + page.get("oldData").get("name").get("value").asText())); + assertTrue(newLeafValueSetNames.contains( + page.get("newData").get("name").get("value").asText())); + } + if (Canonicals.getIdPart(page.get("url").asText()).equals("dxtc")) { + assertTrue(page.get("oldData").get("leafValueSets").isArray()); + assertEquals(3, page.get("oldData").get("leafValueSets").size()); + for (final var leaf : page.get("oldData").get("leafValueSets")) { + var name = leaf.get("name").asText(); + assertTrue(oldLeafValueSetNames.contains(name)); + assertNotNull(leaf.get("codeSystems") + .iterator() + .next() + .get("name") + .asText()); + assertNotNull(leaf.get("codeSystems") + .iterator() + .next() + .get("oid") + .asText()); + } + assertTrue(page.get("newData").get("leafValueSets").isArray()); + assertEquals(3, page.get("newData").get("leafValueSets").size()); + for (final var leaf : page.get("newData").get("leafValueSets")) { + var name = leaf.get("name").asText(); + assertTrue(newLeafValueSetNames.contains(name)); + if (leaf.get("url") + .asText() + .equals( + "https://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion&version")) { + assertTrue(leaf.get("name") + .get("operation") + .get("path") + .asText() + .equals("name")); + assertTrue(leaf.get("name") + .get("operation") + .get("type") + .asText() + .equals("replace")); + } + } + } + } + }); + } + + @Test + void created_deleted_groupers_should_be_visible() throws Exception { + // check that all the grouped leaf valuesets exist + // check that all the expansion contains and compose include get operations + var repository = new InMemoryFhirRepository(FhirContext.forR4()); + createChangelogProcessor = new HapiCreateChangelogProcessor(repository); + + Bundle sourceBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-vsm-gen-grouper-bundle.json"); + Bundle targetBundle = + ClasspathUtil.loadResource(FhirContext.forR4(), Bundle.class, "small-dxtc-modified-diff-bundle.json"); + repository.transaction(sourceBundle); + repository.transaction(targetBundle); + Library source = sourceBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + Library target = targetBundle.getEntry().stream() + .filter(e -> e.getResource() instanceof Library) + .map(e -> (Library) e.getResource()) + .findFirst() + .get(); + + var metadataProperties = List.of("id", "name", "url", "version", "title"); + var versions = List.of( + "Provisional_2022-01-10", + "http://snomed.info/sct/731000124108/version/20240301", + "Provisional_2022-04-25"); + var VSMGrouperCodes = List.of( + "1010333003", + "1010334009", + "106001000119101", + "10692761000119107", + "1177120001", + "123123444111", + "123123444112", + "123123444113"); + var VSMGrouperLeafVsets = List.of("2.16.840.1.113762.1.4.1251.40", "2.16.840.1.113762.1.4.1248.138"); + + ObjectMapper mapper = new ObjectMapper(); + var returnedBinary = (Binary) createChangelogProcessor.createChangelog(source, target, null); + + assertNotNull(returnedBinary); + var node = mapper.readTree(new String(Base64.getDecoder().decode(returnedBinary.getContentAsBase64()))); + assertTrue(node.get("pages").isArray()); + var pages = node.get("pages"); + + // new grouper was deleted + var deletedGrouperPage = StreamUtils.createStreamFromIterator(pages.iterator()) + .filter((page) -> page.get("url").asText().contains("www.test.com")) + .findAny(); + assertTrue(deletedGrouperPage.isPresent()); + + // all codes and properties in the grouper should be "insert" + for (final var property : metadataProperties) { + // all props have a "delete" operation + assertTrue(deletedGrouperPage + .get() + .get("oldData") + .get(property) + .get("operation") + .get("type") + .asText() + .equals("delete")); + } + + assertEquals( + VSMGrouperCodes.size(), + deletedGrouperPage.get().get("oldData").get("codes").size()); + for (final var code : deletedGrouperPage.get().get("oldData").get("codes")) { + // all codes have a "delete" operation + assertTrue(code.get("operation").get("type").asText().equals("delete")); + assertTrue(VSMGrouperCodes.contains(code.get("codeValue").asText())); + assertNotNull(code.get("version").asText()); + assertTrue(versions.contains(code.get("version").asText())); + } + + assertEquals( + VSMGrouperLeafVsets.size(), + deletedGrouperPage.get().get("oldData").get("leafValueSets").size()); + for (final var leaf : deletedGrouperPage.get().get("oldData").get("leafValueSets")) { + // all leaf valuesets have a "delete" operation + assertTrue(leaf.get("operation").get("type").asText().equals("delete")); + assertTrue(VSMGrouperLeafVsets.contains(leaf.get("memberOid").asText())); + } + + // reverse source and target + var returnedBinary2 = (Binary) createChangelogProcessor.createChangelog(target, source, null); + assertNotNull(returnedBinary2); + var node2 = mapper.readTree(new String(Base64.getDecoder().decode(returnedBinary2.getContentAsBase64()))); + assertTrue(node2.get("pages").isArray()); + var pages2 = node2.get("pages"); + + // grouper was created + var createdGrouperPage = StreamUtils.createStreamFromIterator(pages2.iterator()) + .filter((page) -> page.get("url").asText().contains("www.test.com")) + .findAny(); + assertTrue(createdGrouperPage.isPresent()); + // all codes and properties should show as inserted + for (final var property : metadataProperties) { + assertTrue(createdGrouperPage + .get() + .get("newData") + .get(property) + .get("operation") + .get("type") + .asText() + .equals("insert")); + } + + assertEquals( + VSMGrouperCodes.size(), + createdGrouperPage.get().get("newData").get("codes").size()); + for (final var code : createdGrouperPage.get().get("newData").get("codes")) { + assertTrue(code.get("operation").get("type").asText().equals("insert")); + assertTrue(VSMGrouperCodes.contains(code.get("codeValue").asText())); + assertNotNull(code.get("version").asText()); + assertTrue(versions.contains(code.get("version").asText())); + } + + assertEquals( + VSMGrouperLeafVsets.size(), + createdGrouperPage.get().get("newData").get("leafValueSets").size()); + for (final var leaf : createdGrouperPage.get().get("newData").get("leafValueSets")) { + assertTrue(leaf.get("operation").get("type").asText().equals("insert")); + assertTrue(VSMGrouperLeafVsets.contains(leaf.get("memberOid").asText())); + } + } + + private static class CodeAndOperation { + public String code; + public String operation; + + CodeAndOperation(String code, String operation) { + this.code = code; + this.operation = operation; + } + } +} diff --git a/cqf-fhir-cr-hapi/src/test/resources/small-diff-bundle.json b/cqf-fhir-cr-hapi/src/test/resources/small-diff-bundle.json new file mode 100644 index 0000000000..b221ed2f49 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/test/resources/small-diff-bundle.json @@ -0,0 +1,588 @@ +{ + "resourceType": "Bundle", + "id": "rctc-release-2022-10-19-Bundle-rctc", + "type": "transaction", + "timestamp": "2022-10-21T15:18:28.504-04:00", + "entry": [ + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "resource": { + "resourceType": "Library", + "id": "SpecificationLibrary", + "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "version": "2022-10-19", + "status": "active", + "title":"deleted title", + "useContext": [ + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "specification-type" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "program" + } + ] + } + } + ], + "relatedArtifact": [ + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|2.0.0", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ] + }, + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|2022-10-19", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ] + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedRoot|0.1.1" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "767146004" + } + ], + "text": "Toxic effect of arsenic and its compounds (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "000000000" + } + ], + "text": "this will be deleted" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.0" + }, + { + "type": "depends-on", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|2022-10-19" + } + ] + }, + "request": { + "method": "PUT", + "url": "Library/SpecificationLibrary" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "resource": { + "resourceType": "PlanDefinition", + "id": "us-ecr-specification", + "url": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "version": "2.0.0", + "status": "active", + "relatedArtifact": [ + { + "type": "depends-on", + "label": "RCTC Value Set Library of Trigger Codes", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|2022-10-19" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf|0.1.1" + } + ] + }, + "request": { + "method": "PUT", + "url": "PlanDefinition/us-ecr-specification" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "resource": { + "resourceType": "Library", + "id": "rctc", + "url": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "version": "2022-10-19", + "status": "active", + "relatedArtifact": [ + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|2022-10-19", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ] + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf2|0.1.1" + } + ] + }, + "request": { + "method": "PUT", + "url": "Library/rctc" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "resource": { + "resourceType": "ValueSet", + "name":"Diagnosis_ProblemTriggersforPublicHealthReporting", + "id": "dxtc", + "url": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "version": "2022-10-19", + "status": "active", + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program" + }, + "valueReference": { + "reference": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification" + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "priority" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "routine" + } + ] + } + }, + { + "code":{ + "system":"http://terminology.hl7.org/CodeSystem/usage-context-type", + "code":"grouper-type", + "display":"model-grouper" + } + } + ], + "compose": { + "include": [ + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.0" + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X1A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X2A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X3A" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/dxtc" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113762.1.4.1146.6", + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1146.6" + } + ], + "version": "20210526", + "name":"DiphtheriaDisordersSNOMED", + "status": "active", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "concept": [ + { + "code": "1086051000119107", + "display": "Cardiomyopathy due to diphtheria (disorder)" + }, + { + "code": "1086061000119109", + "display": "Diphtheria radiculomyelitis (disorder)" + }, + { + "code": "1086071000119103", + "display": "Diphtheria tubulointerstitial nephropathy (disorder)" + }, + { + "code": "1090211000119102", + "display": "Pharyngeal diphtheria (disorder)" + }, + { + "code": "129667001", + "display": "Diphtheritic peripheral neuritis (disorder)" + }, + { + "code": "13596001", + "display": "Diphtheritic peritonitis (disorder)" + }, + { + "code": "15682004", + "display": "Anterior nasal diphtheria (disorder)" + }, + { + "code": "186347006", + "display": "Diphtheria of penis (disorder)" + }, + { + "code": "18901009", + "display": "Cutaneous diphtheria (disorder)" + }, + { + "code": "194945009", + "display": "Acute myocarditis - diphtheritic (disorder)" + }, + { + "code": "230596007", + "display": "Diphtheritic neuropathy (disorder)" + }, + { + "code": "240422004", + "display": "Tracheobronchial diphtheria (disorder)" + }, + { + "code": "26117009", + "display": "Diphtheritic myocarditis (disorder)" + }, + { + "code": "276197005", + "display": "Infection caused by Corynebacterium diphtheriae (disorder)" + }, + { + "code": "3419005", + "display": "Faucial diphtheria (disorder)" + }, + { + "code": "397428000", + "display": "Diphtheria (disorder)" + }, + { + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae (disorder)" + }, + { + "code": "48278001", + "display": "Diphtheritic cystitis (disorder)" + }, + { + "code": "50215002", + "display": "Laryngeal diphtheria (disorder)" + }, + { + "code": "715659006", + "display": "Diphtheria of respiratory system (disorder)" + }, + { + "code": "75589004", + "display": "Nasopharyngeal diphtheria (disorder)" + }, + { + "code": "7773002", + "display": "Conjunctival diphtheria (disorder)" + }, + { + "code": "789005009", + "display": "Paralysis of uvula after diphtheria (disorder)" + } + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086051000119107" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086061000119109" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086071000119103" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113762.1.4.1146.6" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113883.3.464.1003.113.11.1090", + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113883.3.464.1003.113.11.1090" + } + ], + "version": "20180310", + "name":"AnkylosingSpondylitis", + "status": "active", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "concept": [ + { + "code": "772155008", + "display": "Acute poliomyelitis suspected (situation)" + } + ] + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.0", + "resource": { + "resourceType": "ValueSet", + "id": "fake.oid.to.trigger.naive.expansion", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-triggering-valueset" + ] + }, + "text": { + "status": "extensions", + "div": "

Generated Narrative: ValueSet

Resource ValueSet \"fake.oid.to.trigger.naive.expansion\"

Profile: US Public Health Triggering ValueSet

author: CSTE Author:

steward: CSTE Steward:

url: http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion

identifier: id: urn:oid:fake.oid.to.trigger.naive.expansion

version: 1.0.0

name: AcanthamoebaDiseaseKeratitisDisordersSNOMED

title: Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)

status: active

experimental: true

publisher: eCR

description: Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)

compose

include

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

concept

code: 127631000119105

display: Corneal ulcer due to acanthamoeba (disorder)

concept

code: 15693201000119102

display: Keratitis of bilateral eyes caused by Acanthamoeba (disorder)

concept

code: 15693241000119100

display: Keratitis of left eye caused by Acanthamoeba (disorder)

concept

code: 15693281000119105

display: Keratitis of right eye caused by Acanthamoeba (disorder)

concept

code: 15698841000119105

display: Ulcer of right cornea caused by Acanthamoeba (disorder)

concept

code: 15698881000119100

display: Ulcer of left cornea caused by Acanthamoeba (disorder)

concept

code: 231896005

display: Acanthamoeba keratitis (disorder)

concept

code: 711645008

display: Corneal ulcer caused by Acanthamoeba (disorder)

concept

code: 840444002

display: Dacryoadenitis due to Acanthamoeba keratitis (disorder)

concept

code: 840484006

display: Conjunctivitis caused by Acanthamoeba (disorder)

expansion

timestamp: 2022-04-05 10:06:43-0400

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 127631000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693201000119102

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693241000119100

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693281000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15698841000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15698881000119100

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 231896005

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 711645008

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 840444002

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 840484006

" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-author", + "valueContactDetail": { + "name": "CSTE Author" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-steward", + "valueContactDetail": { + "name": "CSTE Steward" + } + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:fake.oid.to.trigger.naive.expansion" + } + ], + "version": "1.0.0", + "name": "AcanthamoebaDiseaseKeratitisDisordersSNOMED", + "title": "Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)", + "status": "active", + "experimental": true, + "publisher": "eCR", + "description": "Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "concept": [ + { + "code": "127631000119105", + "display": "Corneal ulcer due to acanthamoeba (disorder)" + }, + { + "code": "15693281000119105", + "display": "Keratitis of right eye caused by Acanthamoeba (disorder)" + } + ] + } + ] + }, + "expansion": { + "timestamp": "2022-04-05T10:06:43-04:00", + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "127631000119105" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "15693281000119105" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/fake.oid.to.trigger.naive.expansion", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion&version=1.0.0" + } + } + ] +} diff --git a/cqf-fhir-cr-hapi/src/test/resources/small-dxtc-modified-diff-bundle.json b/cqf-fhir-cr-hapi/src/test/resources/small-dxtc-modified-diff-bundle.json new file mode 100644 index 0000000000..63b0930d49 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/test/resources/small-dxtc-modified-diff-bundle.json @@ -0,0 +1,669 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary|1.0.0-draft", + "resource": { + "resourceType": "Library", + "id": "7", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE" + }, + "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "version": "1.0.0-draft", + "status": "draft", + "name": "Updated name", + "purpose": "UpdatedPurpose", + "effectivePeriod":{ + "start":"2020-10-01", + "end":"2025-10-01" + }, + "useContext": [ + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "specification-type" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "program" + } + ] + } + } + ], + "relatedArtifact": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|1.0.0-draft" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.0.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedRoot|0.1.1" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "123123123" + } + ] + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163|20220603" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "767146004" + } + ], + "text": "Toxic effect of arsenic and its compounds (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.1" + }, + { + "type": "depends-on", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.0.0-draft" + }, + { + "type": "depends-on", + "resource": "http://snomed.info/sct" + } + ] + }, + "request": { + "method": "POST", + "url": "Library/7", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary&version=1.0.0-draft" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|1.0.0-draft", + "resource": { + "resourceType": "PlanDefinition", + "id": "8", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE" + }, + "url": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "version": "1.0.0-draft", + "status": "draft", + "relatedArtifact": [ + { + "type": "depends-on", + "label": "RCTC Value Set Library of Trigger Codes", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.0.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf|0.1.1" + } + ] + }, + "request": { + "method": "POST", + "url": "PlanDefinition/8", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification&version=1.0.0-draft" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.0.0-draft", + "resource": { + "resourceType": "Library", + "id": "9", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE" + }, + "url": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "version": "1.0.0-draft", + "status": "draft", + "relatedArtifact": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.0.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf2|0.1.1" + } + ] + }, + "request": { + "method": "POST", + "url": "Library/9", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/Library/rctc&version=1.0.0-draft" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.0.0-draft", + "resource": { + "resourceType": "ValueSet", + "id": "10", + "name":"Diagnosis_ProblemTriggersforPublicHealthReporting", + "meta": { + "versionId": "2", + "lastUpdated": "2023-12-14T15:43:06.193-05:00", + "source": "#YvcttWKq2KbM0Igj" + }, + "url": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "version": "1.0.0-draft", + "status": "draft", + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program" + }, + "valueReference": { + "reference": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification" + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "priority" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "routine" + } + ] + } + }, + { + "code":{ + "system":"http://terminology.hl7.org/CodeSystem/usage-context-type", + "code":"grouper-type", + "display":"model-grouper" + } + } + ], + "compose": { + "include": [ + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163" + ] + },{ + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.1" + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X1A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X2A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X3A" + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet/10", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/ValueSet/dxtc&version=1.0.0-draft" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113883.3.464.1003.113.11.1090", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:33.243-05:00", + "source": "#MT6tY32vbfEfzQmh" + }, + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113883.3.464.1003.113.11.1090" + } + ], + "version": "20180310", + "status": "active", + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "focus" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "priority" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "routine" + } + ], + "text": "Routine" + } + } + ], + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "concept": [ + { + "code": "772155008", + "display": "Acute poliomyelitis suspected (situation)" + } + ] + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090&version=20180310" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163|20220603", + "resource": { + "resourceType": "ValueSet", + "id": "11", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE", + "profile": [ + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/publishable-valueset-cqfm", + "http://hl7.org/fhir/StructureDefinition/shareablevalueset" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-author", + "valueString": "CSTE Author" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-keyWord", + "valueString": "Cholera,G_Enteric,Trigger" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/resource-lastReviewDate", + "valueDate": "2022-12-15" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-effectiveDate", + "valueDate": "2022-06-03" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-authoritativeSource", + "valueUri": "http://cts.nlm.nih.gov/fhir" + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1146.163" + } + ], + "version": "20220603", + "name": "Cholera (Disorders) (SNOMED)", + "title": "Cholera (Disorders) (SNOMED)", + "status": "active", + "experimental": false, + "date": "2022-06-03T01:06:35-04:00", + "publisher": "CSTE Steward", + "jurisdiction": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + ], + "purpose": "(Clinical Focus: This set of values contains diagnoses or problems that represent that the patient has Cholera regardless of the clinical presentation of the condition),(Data Element Scope: Diagnoses or problems documented in a clinical record.),(Inclusion Criteria: Root1 = Cholera (disorder); \nRoot1 children included = All;\n\nAdded leaf concepts: YES\n\nRoot2 = Intestinal infection due to Vibrio cholerae O1 (disorder); \nRoot2 children included = All;),(Exclusion Criteria: Cholera non-O159 or non-O1; Verner–Morrison syndrome; Cholera vaccine related disorders)", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "1.2.3", + "concept": [ + { + "code": "1193749009", + "display": "Inflammation of small intestine caused by Vibrio cholerae (disorder)" + }, + { + "code": "1193750009", + "display": "Inflammation of intestine caused by Vibrio cholerae (disorder)" + }, + { + "code": "240349003", + "display": "Cholera caused by Vibrio cholerae O1 Classical biotype (disorder)" + }, + { + "code": "240350003", + "display": "Cholera - non-O1 group vibrio (disorder)" + }, + { + "code": "240351004", + "display": "Cholera - O139 group Vibrio cholerae (disorder)" + }, + { + "code": "447282003", + "display": "Intestinal infection caused by Vibrio cholerae O1 (disorder)" + }, + { + "code": "63650001", + "display": "Cholera (disorder)" + }, + { + "code": "81020007", + "display": "Cholera caused by Vibrio cholerae El Tor (disorder)" + } + ] + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet/11", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163&version=20220603" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.1", + "resource": { + "resourceType": "ValueSet", + "id": "fake.oid.to.trigger.naive.expansion", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-triggering-valueset" + ] + }, + "text": { + "status": "extensions", + "div": "

Generated Narrative: ValueSet

Resource ValueSet \"fake.oid.to.trigger.naive.expansion\"

Profile: US Public Health Triggering ValueSet

author: CSTE Author:

steward: CSTE Steward:

url: http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion

identifier: id: urn:oid:fake.oid.to.trigger.naive.expansion

version: 1.0.0

name: AcanthamoebaDiseaseKeratitisDisordersSNOMED

title: Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)

status: active

experimental: true

publisher: eCR

description: Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)

compose

include

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

concept

code: 127631000119105

display: Corneal ulcer due to acanthamoeba (disorder)

concept

code: 15693201000119102

display: Keratitis of bilateral eyes caused by Acanthamoeba (disorder)

concept

code: 15693241000119100

display: Keratitis of left eye caused by Acanthamoeba (disorder)

concept

code: 15693281000119105

display: Keratitis of right eye caused by Acanthamoeba (disorder)

concept

code: 15698841000119105

display: Ulcer of right cornea caused by Acanthamoeba (disorder)

concept

code: 15698881000119100

display: Ulcer of left cornea caused by Acanthamoeba (disorder)

concept

code: 231896005

display: Acanthamoeba keratitis (disorder)

concept

code: 711645008

display: Corneal ulcer caused by Acanthamoeba (disorder)

concept

code: 840444002

display: Dacryoadenitis due to Acanthamoeba keratitis (disorder)

concept

code: 840484006

display: Conjunctivitis caused by Acanthamoeba (disorder)

expansion

timestamp: 2022-04-05 10:06:43-0400

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 127631000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693201000119102

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693241000119100

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693281000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15698841000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15698881000119100

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 231896005

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 711645008

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 840444002

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 840484006

" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-author", + "valueContactDetail": { + "name": "CSTE Author" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-steward", + "valueContactDetail": { + "name": "CSTE Steward" + } + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:fake.oid.to.trigger.naive.expansion" + } + ], + "version": "1.0.1", + "name": "UpdatedName", + "title": "Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)", + "status": "active", + "experimental": true, + "publisher": "eCR", + "description": "Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "concept": [ + { + "code": "127631000119105", + "display": "Corneal ulcer due to acanthamoeba (disorder)" + }, + { + "code": "15693201000119102", + "display": "Keratitis of bilateral eyes caused by Acanthamoeba (disorder)" + }, + { + "code": "15693241000119100", + "display": "Keratitis of left eye caused by Acanthamoeba (disorder)" + } + ] + } + ] + }, + "expansion": { + "timestamp": "2022-04-05T10:06:43-04:00", + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "127631000119105" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "15693201000119102" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "15693241000119100" + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion&version=1.0.1" + } + }, + { + "fullUrl": "http://snomed.info/sct|PROVISIONAL", + "resource": { + "resourceType": "CodeSystem", + "meta": { + "versionId": "1", + "lastUpdated": "2024-08-30T00:37:41.218+00:00", + "source": "#X2EAyHiQ0qeWPiLd", + "tag": [ + { + "system": "http://aphl.org/fhir/vsm/CodeSystem/vsm-workflow-codes", + "code": "vsm-authored" + }, + { + "system": "http://aphl.org/fhir/vsm/CodeSystem/vsm-workflow-codes", + "code": "vsm-provisional" + } + ] + }, + "url": "http://snomed.info/sct", + "version": "PROVISIONAL", + "name": "SNOMEDCT", + "status": "draft", + "experimental": true, + "content": "complete", + "concept": [ + { + "code": "e12e21", + "display": "e12e21e2", + "definition": "12e12e12e21" + } + ] + }, + "request": { + "method": "POST", + "url": "CodeSystem", + "ifNoneExist": "url=http://snomed.info/sct" + } + } + ] +} diff --git a/cqf-fhir-cr-hapi/src/test/resources/small-vsm-gen-grouper-bundle.json b/cqf-fhir-cr-hapi/src/test/resources/small-vsm-gen-grouper-bundle.json new file mode 100644 index 0000000000..ff62837875 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/test/resources/small-vsm-gen-grouper-bundle.json @@ -0,0 +1,981 @@ +{ + "resourceType": "Bundle", + "id": "rctc-release-2022-10-19-Bundle-rctc", + "type": "transaction", + "timestamp": "2022-10-21T15:18:28.504-04:00", + "entry": [ + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "resource": { + "resourceType": "Library", + "id": "SpecificationLibrary", + "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "version": "2022-10-19", + "status": "active", + "title":"deleted title", + "useContext": [ + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "specification-type" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "program" + } + ] + } + } + ], + "relatedArtifact": [ + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|2.0.0", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ] + }, + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|2022-10-19", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ] + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedRoot|0.1.1" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "767146004" + } + ], + "text": "Toxic effect of arsenic and its compounds (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "715174007" + } + ], + "text": "Carbapenem-resistant Acinetobacter baumannii (CRAB)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "routine" + } + ], + "text": "Routine" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1251.40|20231001" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "999999999999999" + } + ], + "text": "unknown condition help call House" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1248.138|20240120" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "000000000" + } + ], + "text": "this will be deleted" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.0" + }, + { + "type": "depends-on", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|2022-10-19" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "routine" + } + ], + "text": "Routine" + } + } + ], + "type": "depends-on", + "resource": "http://www.test.com/fhir/ValueSet/VSMGeneratedGrouper2|1.2.0-draft" + } + ] + }, + "request": { + "method": "PUT", + "url": "Library/SpecificationLibrary" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "resource": { + "resourceType": "PlanDefinition", + "id": "us-ecr-specification", + "url": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "version": "2.0.0", + "status": "active", + "relatedArtifact": [ + { + "type": "depends-on", + "label": "RCTC Value Set Library of Trigger Codes", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|2022-10-19" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf|0.1.1" + } + ] + }, + "request": { + "method": "PUT", + "url": "PlanDefinition/us-ecr-specification" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "resource": { + "resourceType": "Library", + "id": "rctc", + "url": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "version": "2022-10-19", + "status": "active", + "relatedArtifact": [ + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|2022-10-19", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/artifact-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://www.test.com/fhir/ValueSet/VSMGeneratedGrouper2|1.2.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf2|0.1.1" + } + ] + }, + "request": { + "method": "PUT", + "url": "Library/rctc" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|2022-10-19", + "resource": { + "resourceType": "ValueSet", + "name":"Diagnosis_ProblemTriggersforPublicHealthReporting", + "id": "dxtc", + "url": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "version": "2022-10-19", + "status": "active", + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program" + }, + "valueReference": { + "reference": "PlanDefinition/us-ecr-specification" + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code":{ + "system":"http://terminology.hl7.org/CodeSystem/usage-context-type", + "code":"grouper-type", + "display":"model-grouper" + } + } + ], + "compose": { + "include": [ + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.0" + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X1A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X2A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X3A" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/dxtc" + } + }, + { + "fullUrl": "http://www.test.com/fhir/ValueSet/VSMGeneratedGrouper2|1.2.0-draft", + "resource": { + "resourceType": "ValueSet", + "meta": { + "lastUpdated": "2024-07-08T23:14:42.386+00:00", + "profile": [ + "http://aphl.org/fhir/vsm/StructureDefinition/vsm-groupervalueset", + "http://hl7.org/fhir/us/ecr/StructureDefinition/ersd-valueset", + "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-triggering-valueset" + ], + "tag": [ + { + "system": "http://aphl.org/fhir/vsm/CodeSystem/vsm-workflow-codes", + "code": "vsm-authored" + } + ] + }, + "extension": [ + { + "url": "http://www.test.com/fhir/StructureDefinition/valueset-author", + "valueContactDetail": { + "name": "CSTE Author" + } + } + ], + "url": "http://www.test.com/fhir/ValueSet/VSMGeneratedGrouper2", + "version": "1.2.0-draft", + "name": "VSMGeneratedGrouper2", + "title": "VSM-Generated Grouper 2", + "status": "draft", + "experimental": true, + "publisher": "CSTE Steward", + "description": "I am describing a VSM Grouper", + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "program" + }, + "valueReference": { + "reference": "PlanDefinition/us-ecr-specification" + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://aphl.org/fhir/vsm/CodeSystem/usage-context-type", + "code": "grouper-type" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://aphl.org/fhir/vsm/CodeSystem/usage-context-type", + "code": "model-grouper" + } + ], + "text": "Model Grouper" + } + } + ], + "purpose": "I am a VSM Grouper", + "compose": { + "include": [ + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1251.40" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1248.138|20240120" + ] + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113762.1.4.1146.6", + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1146.6" + } + ], + "version": "20210526", + "name":"DiphtheriaDisordersSNOMED", + "status": "active", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "concept": [ + { + "code": "1086051000119107", + "display": "Cardiomyopathy due to diphtheria (disorder)" + }, + { + "code": "1086061000119109", + "display": "Diphtheria radiculomyelitis (disorder)" + }, + { + "code": "1086071000119103", + "display": "Diphtheria tubulointerstitial nephropathy (disorder)" + }, + { + "code": "1090211000119102", + "display": "Pharyngeal diphtheria (disorder)" + }, + { + "code": "129667001", + "display": "Diphtheritic peripheral neuritis (disorder)" + }, + { + "code": "13596001", + "display": "Diphtheritic peritonitis (disorder)" + }, + { + "code": "15682004", + "display": "Anterior nasal diphtheria (disorder)" + }, + { + "code": "186347006", + "display": "Diphtheria of penis (disorder)" + }, + { + "code": "18901009", + "display": "Cutaneous diphtheria (disorder)" + }, + { + "code": "194945009", + "display": "Acute myocarditis - diphtheritic (disorder)" + }, + { + "code": "230596007", + "display": "Diphtheritic neuropathy (disorder)" + }, + { + "code": "240422004", + "display": "Tracheobronchial diphtheria (disorder)" + }, + { + "code": "26117009", + "display": "Diphtheritic myocarditis (disorder)" + }, + { + "code": "276197005", + "display": "Infection caused by Corynebacterium diphtheriae (disorder)" + }, + { + "code": "3419005", + "display": "Faucial diphtheria (disorder)" + }, + { + "code": "397428000", + "display": "Diphtheria (disorder)" + }, + { + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae (disorder)" + }, + { + "code": "48278001", + "display": "Diphtheritic cystitis (disorder)" + }, + { + "code": "50215002", + "display": "Laryngeal diphtheria (disorder)" + }, + { + "code": "715659006", + "display": "Diphtheria of respiratory system (disorder)" + }, + { + "code": "75589004", + "display": "Nasopharyngeal diphtheria (disorder)" + }, + { + "code": "7773002", + "display": "Conjunctival diphtheria (disorder)" + }, + { + "code": "789005009", + "display": "Paralysis of uvula after diphtheria (disorder)" + } + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086051000119107" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086061000119109" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086071000119103" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113762.1.4.1146.6" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113883.3.464.1003.113.11.1090", + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113883.3.464.1003.113.11.1090" + } + ], + "version": "20180310", + "name":"AnkylosingSpondylitis", + "status": "active", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "concept": [ + { + "code": "772155008", + "display": "Acute poliomyelitis suspected (situation)" + } + ] + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion|1.0.0", + "resource": { + "resourceType": "ValueSet", + "id": "fake.oid.to.trigger.naive.expansion", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-triggering-valueset" + ] + }, + "text": { + "status": "extensions", + "div": "

Generated Narrative: ValueSet

Resource ValueSet \"fake.oid.to.trigger.naive.expansion\"

Profile: US Public Health Triggering ValueSet

author: CSTE Author:

steward: CSTE Steward:

url: http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion

identifier: id: urn:oid:fake.oid.to.trigger.naive.expansion

version: 1.0.0

name: AcanthamoebaDiseaseKeratitisDisordersSNOMED

title: Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)

status: active

experimental: true

publisher: eCR

description: Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)

compose

include

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

concept

code: 127631000119105

display: Corneal ulcer due to acanthamoeba (disorder)

concept

code: 15693201000119102

display: Keratitis of bilateral eyes caused by Acanthamoeba (disorder)

concept

code: 15693241000119100

display: Keratitis of left eye caused by Acanthamoeba (disorder)

concept

code: 15693281000119105

display: Keratitis of right eye caused by Acanthamoeba (disorder)

concept

code: 15698841000119105

display: Ulcer of right cornea caused by Acanthamoeba (disorder)

concept

code: 15698881000119100

display: Ulcer of left cornea caused by Acanthamoeba (disorder)

concept

code: 231896005

display: Acanthamoeba keratitis (disorder)

concept

code: 711645008

display: Corneal ulcer caused by Acanthamoeba (disorder)

concept

code: 840444002

display: Dacryoadenitis due to Acanthamoeba keratitis (disorder)

concept

code: 840484006

display: Conjunctivitis caused by Acanthamoeba (disorder)

expansion

timestamp: 2022-04-05 10:06:43-0400

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 127631000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693201000119102

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693241000119100

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15693281000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15698841000119105

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 15698881000119100

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 231896005

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 711645008

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 840444002

contains

system: SNOMED CT (all versions)

version: Provisional_2022-01-10

code: 840484006

" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-author", + "valueContactDetail": { + "name": "CSTE Author" + } + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-steward", + "valueContactDetail": { + "name": "CSTE Steward" + } + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:fake.oid.to.trigger.naive.expansion" + } + ], + "version": "1.0.0", + "name": "AcanthamoebaDiseaseKeratitisDisordersSNOMED", + "title": "Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)", + "status": "active", + "experimental": true, + "publisher": "eCR", + "description": "Acanthamoeba Disease [Keratitis] (Disorders) (SNOMED)", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "concept": [ + { + "code": "127631000119105", + "display": "Corneal ulcer due to acanthamoeba (disorder)" + }, + { + "code": "15693281000119105", + "display": "Keratitis of right eye caused by Acanthamoeba (disorder)" + } + ] + } + ] + }, + "expansion": { + "timestamp": "2022-04-05T10:06:43-04:00", + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "127631000119105" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-01-10", + "code": "15693281000119105" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/fake.oid.to.trigger.naive.expansion", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/fake.oid.to.trigger.naive.expansion&version=1.0.0" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1251.40|20231001", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113762.1.4.1251.40", + "meta": { + "versionId": "10", + "lastUpdated": "2023-12-21T17:43:03.000-05:00", + "profile": [ + "http://hl7.org/fhir/StructureDefinition/shareablevalueset", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/computable-valueset-cqfm", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/publishable-valueset-cqfm" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-effectiveDate", + "valueDate": "2023-10-01" + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1251.40", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1251.40" + } + ], + "version": "20231001", + "name": "ChronicObstructivePulmonaryDisease", + "title": "Chronic Obstructive Pulmonary Disease", + "status": "active", + "date": "2023-10-01T01:01:17-04:00", + "publisher": "UTSW Clinical Informatics Center Steward", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "concept": [ + { + "code": "1010333003", + "display": "Emphysema of left lung (disorder)" + }, + { + "code": "1010334009", + "display": "Emphysema of right lung (disorder)" + },{ + "code": "106001000119101", + "display": "Chronic obstructive pulmonary disease with acute bronchitis (disorder)" + },{ + "code": "10692761000119107", + "display": "Asthma-chronic obstructive pulmonary disease overlap syndrome (disorder)" + },{ + "code": "1177120001", + "display": "Bronchiolitis obliterans syndrome due to and following allogeneic stem cell transplant (disorder)" + } + ] + } + ] + }, + "expansion": { + "identifier": "urn:uuid:547ae256-8987-4afb-910e-8b2e613df5ee", + "timestamp": "2024-07-10T12:56:43-04:00", + "total": 46, + "offset": 0, + "parameter": [ + { + "name": "count", + "valueInteger": 1000 + }, + { + "name": "offset", + "valueInteger": 0 + } + ], + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "1010333003", + "display": "Emphysema of left lung (disorder)" + }, + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "1010334009", + "display": "Emphysema of right lung (disorder)" + }, + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "106001000119101", + "display": "Chronic obstructive pulmonary disease with acute bronchitis (disorder)" + }, + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "10692761000119107", + "display": "Asthma-chronic obstructive pulmonary disease overlap syndrome (disorder)" + }, + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "1177120001", + "display": "Bronchiolitis obliterans syndrome due to and following allogeneic stem cell transplant (disorder)" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113762.1.4.1251.40", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1251.40&version=20231001" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1248.138|20240120", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113762.1.4.1248.138", + "meta": { + "versionId": "10", + "lastUpdated": "2023-12-21T17:43:03.000-05:00", + "profile": [ + "http://hl7.org/fhir/StructureDefinition/shareablevalueset", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/computable-valueset-cqfm", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/publishable-valueset-cqfm" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-effectiveDate", + "valueDate": "2023-10-01" + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1248.138", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1248.138" + } + ], + "version": "20240120", + "name": "COVID something", + "title": "COVID COVID COVID", + "status": "active", + "date": "2023-10-01T01:01:17-04:00", + "publisher": "UTSW Clinical Informatics Center Steward", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "concept": [ + { + "code": "123123444111", + "display": "Some covid disorder" + }, + { + "code": "123123444112", + "display": "a second covid disorder" + },{ + "code": "123123444113", + "display": "3 covids" + } + ] + } + ] + }, + "expansion": { + "identifier": "urn:uuid:547ae256-8987-4afb-910e-8b2e613df5ee", + "timestamp": "2024-07-10T12:56:43-04:00", + "total": 46, + "offset": 0, + "parameter": [ + { + "name": "count", + "valueInteger": 1000 + }, + { + "name": "offset", + "valueInteger": 0 + } + ], + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "123123444111", + "display": "Some covid disorder" + }, + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "123123444112", + "display": "a second covid disorder" + }, + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/731000124108/version/20240301", + "code": "123123444113", + "display": "3 covids" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113762.1.4.1248.138", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1248.138&version=20240120" + } + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ArtifactDiffProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ArtifactDiffProcessor.java index f64c42ba5f..c9d7f8da3e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ArtifactDiffProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ArtifactDiffProcessor.java @@ -34,8 +34,8 @@ public IBaseParameters getArtifactDiff( } public static class DiffCache { - private final Map diffs = new HashMap(); - private final Map resources = new HashMap(); + private final Map diffs = new HashMap<>(); + private final Map resources = new HashMap<>(); public DiffCache() { super(); @@ -79,6 +79,14 @@ public List getResourcesForUrl(String url) { .toList(); } + public Optional getSourceResourceForUrl(String url) { + return getResourcesForUrl(url).stream().filter(res -> res.isSource).findFirst(); + } + + public Optional getTargetResourceForUrl(String url) { + return getResourcesForUrl(url).stream().filter(res -> !res.isSource).findFirst(); + } + public static class DiffCacheResource { public final MetadataResource resource; public final boolean isSource; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/CreateChangelogProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/CreateChangelogProcessor.java new file mode 100644 index 0000000000..58390a2c05 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/CreateChangelogProcessor.java @@ -0,0 +1,1374 @@ +package org.opencds.cqf.fhir.cr.common; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.PlanDefinition; +import org.hl7.fhir.r4.model.PrimitiveType; +import org.hl7.fhir.r4.model.RelatedArtifact; +import org.hl7.fhir.r4.model.UsageContext; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; +import org.opencds.cqf.fhir.cr.common.ArtifactDiffProcessor.DiffCache; +import org.opencds.cqf.fhir.cr.common.CreateChangelogProcessor.ChangeLog.ValueSetChild.Code; +import org.opencds.cqf.fhir.cr.crmi.TransformProperties; +import org.opencds.cqf.fhir.utility.Canonicals; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* There are a number of getters that are detected as unused, but they are invoked during the +changelog process and their removal affects the operation outcome. */ +@SuppressWarnings("unused") +public class CreateChangelogProcessor implements ICreateChangelogProcessor { + + private static final Logger logger = LoggerFactory.getLogger(CreateChangelogProcessor.class); + + public CreateChangelogProcessor() { + /* Empty as we will not perform create changelog outside HAPI context */ + } + + @Override + public IBaseResource createChangelog( + IBaseResource source, IBaseResource target, IBaseResource terminologyEndpoint) { + logger.info("Unable to perform $create-changelog outside of HAPI context"); + return new Parameters(); + } + + @SuppressWarnings("rawtypes") + public static class ChangeLog { + private List pages; + private String manifestUrl; + public static final String URLS_DONT_MATCH = "URLs don't match"; + public static final String WRONG_TYPE = "wrong type"; + public static final String REPLACE = "replace"; + public static final String INSERT = "insert"; + public static final String DELETE = "delete"; + + public ChangeLog(String url) { + this.pages = new ArrayList<>(); + this.manifestUrl = url; + } + + public List getPages() { + return pages; + } + + public void setPages(List pages) { + this.pages = pages; + } + + public String getManifestUrl() { + return manifestUrl; + } + + public void setManifestUrl(String manifestUrl) { + this.manifestUrl = manifestUrl; + } + + public Page addPage(ValueSet sourceResource, ValueSet targetResource, DiffCache cache) + throws UnprocessableEntityException { + if (sourceResource != null + && targetResource != null + && !sourceResource.getUrl().equals(targetResource.getUrl())) { + throw new UnprocessableEntityException(URLS_DONT_MATCH); + } + // Map< [Code], [Object with code, version, system, etc.] > + Map codeMap = new HashMap<>(); + // Map< [URL], Map <[Version], [Object with name, version, and other metadata] >> + Map> leafMetadataMap = new HashMap<>(); + updateCodeMapAndLeafMetadataMap(codeMap, leafMetadataMap, sourceResource, cache); + updateCodeMapAndLeafMetadataMap(codeMap, leafMetadataMap, targetResource, cache); + var oldData = sourceResource == null + ? null + : new ValueSetChild( + sourceResource.getTitle(), + sourceResource.getIdPart(), + sourceResource.getVersion(), + sourceResource.getName(), + sourceResource.getUrl(), + sourceResource.getCompose().getInclude(), + sourceResource.getExpansion().getContains(), + codeMap, + leafMetadataMap, + getPriority(sourceResource).orElse(null)); + var newData = targetResource == null + ? null + : new ValueSetChild( + targetResource.getTitle(), + targetResource.getIdPart(), + targetResource.getVersion(), + targetResource.getName(), + targetResource.getUrl(), + targetResource.getCompose().getInclude(), + targetResource.getExpansion().getContains(), + codeMap, + leafMetadataMap, + getPriority(targetResource).orElse(null)); + var url = getPageUrl(sourceResource, targetResource); + var page = new Page<>(url, oldData, newData); + this.pages.add(page); + return page; + } + + public String getPageUrl(MetadataResource source, MetadataResource target) { + if (source == null) { + return target.getUrl(); + } + return source.getUrl(); + } + + private Optional getPriority(ValueSet valueSet) { + return valueSet.getUseContext().stream() + .filter(uc -> uc.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) + && uc.getCode().getCode().equals(TransformProperties.VSM_PRIORITY_CODE)) + .findAny() + .map(uc -> uc.getValueCodeableConcept().getCodingFirstRep().getCode()); + } + + private void updateCodeMapAndLeafMetadataMap( + Map codeMap, + Map> leafMap, + ValueSet valueSet, + DiffCache cache) { + if (valueSet != null) { + var leafData = updateLeafMap(leafMap, valueSet); + if (valueSet.getCompose().hasInclude()) { + handleValueSetInclude(codeMap, leafMap, valueSet, cache, leafData); + } + if (valueSet.getExpansion().hasContains()) { + handleValueSetContains(codeMap, valueSet, leafData); + } + } + } + + private void handleValueSetInclude( + Map codeMap, + Map> leafMap, + ValueSet valueSet, + DiffCache cache, + ValueSetChild.Leaf leafData) { + valueSet.getCompose().getInclude().forEach(concept -> { + if (concept.hasConcept()) { + updateLeafData(concept.getSystem(), leafData); + mapConceptSetToCodeMap( + codeMap, + concept, + Canonicals.getIdPart(valueSet.getUrl()), + valueSet.getName(), + valueSet.getTitle(), + valueSet.getUrl()); + } + if (concept.hasValueSet()) { + concept.getValueSet().stream() + .map(vs -> cache.getResource(vs.getValue()).map(v -> (ValueSet) v)) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(vs -> { + updateLeafMap(leafMap, vs); + updateCodeMapAndLeafMetadataMap(codeMap, leafMap, vs, cache); + }); + } + }); + } + + private void handleValueSetContains(Map codeMap, ValueSet valueSet, ValueSetChild.Leaf leafData) { + valueSet.getExpansion().getContains().forEach(cnt -> { + if (!codeMap.containsKey(cnt.getCode())) { + updateLeafData(cnt.getSystem(), leafData); + mapExpansionContainsToCodeMap( + codeMap, + cnt, + Canonicals.getIdPart(valueSet.getUrl()), + valueSet.getName(), + valueSet.getTitle(), + valueSet.getUrl()); + } + }); + } + + private static void updateLeafData(String system, ValueSetChild.Leaf leafData) { + var codeSystemName = Code.getCodeSystemName(system); + var codeSystemOid = Code.getCodeSystemOid(system); + var doesOidExistInList = leafData.codeSystems.stream() + .anyMatch(nameAndOid -> nameAndOid.oid != null && nameAndOid.oid.equals(codeSystemOid)); + if (!doesOidExistInList) { + leafData.codeSystems.add(new ValueSetChild.Leaf.NameAndOid(codeSystemName, codeSystemOid)); + } + } + + private ValueSetChild.Leaf updateLeafMap( + Map> leafMap, ValueSet valueSet) + throws UnprocessableEntityException { + if (!valueSet.hasVersion()) { + throw new UnprocessableEntityException("ValueSet " + valueSet.getUrl() + " does not have a version"); + } + + var versionedLeafMap = leafMap.get(valueSet.getUrl()); + + if (!leafMap.containsKey(valueSet.getUrl())) { + versionedLeafMap = new HashMap<>(); + leafMap.put(valueSet.getUrl(), versionedLeafMap); + } + + var leaf = versionedLeafMap.get(valueSet.getVersion()); + if (!versionedLeafMap.containsKey(valueSet.getVersion())) { + leaf = new ValueSetChild.Leaf( + Canonicals.getIdPart(valueSet.getUrl()), + valueSet.getName(), + valueSet.getTitle(), + valueSet.getUrl(), + valueSet.getStatus()); + versionedLeafMap.put(valueSet.getVersion(), leaf); + } + return leaf; + } + + private void mapExpansionContainsToCodeMap( + Map codeMap, + ValueSet.ValueSetExpansionContainsComponent containsComponent, + String source, + String name, + String title, + String url) { + var system = containsComponent.getSystem(); + var id = containsComponent.getId(); + var version = containsComponent.getVersion(); + var codeValue = containsComponent.getCode(); + var display = containsComponent.getDisplay(); + var code = new ValueSetChild.Code(id, system, codeValue, version, display, source, name, title, url, null); + codeMap.put(codeValue, code); + } + // can this be done with a fhir operation? tx server work? + private void mapConceptSetToCodeMap( + Map codeMap, + ValueSet.ConceptSetComponent concept, + String source, + String name, + String title, + String url) { + var system = concept.getSystem(); + var id = concept.getId(); + var version = concept.getVersion(); + concept.getConcept().stream() + .filter(ValueSet.ConceptReferenceComponent::hasCode) + .forEach(conceptReference -> { + if (!codeMap.containsKey(conceptReference.getCode())) { + var code = new ValueSetChild.Code( + id, + system, + conceptReference.getCode(), + version, + conceptReference.getDisplay(), + source, + name, + title, + url, + null); + codeMap.put(conceptReference.getCode(), code); + } + }); + } + + public Page addPage(Library sourceResource, Library targetResource) + throws UnprocessableEntityException { + if (sourceResource != null + && targetResource != null + && !sourceResource.getUrl().equals(targetResource.getUrl())) { + throw new UnprocessableEntityException(URLS_DONT_MATCH); + } + var oldData = getLibraryChild(sourceResource); + var newData = getLibraryChild(targetResource); + var url = getPageUrl(sourceResource, targetResource); + var page = new Page<>(url, oldData, newData); + this.pages.add(page); + return page; + } + + private static LibraryChild getLibraryChild(Library library) { + return library == null + ? null + : new LibraryChild( + library.getName(), + library.getPurpose(), + library.getTitle(), + library.getIdPart(), + library.getVersion(), + library.getUrl(), + Optional.ofNullable(library.getEffectivePeriod()) + .map(Period::getStart) + .map(Date::toString) + .orElse(null), + Optional.ofNullable(library.getApprovalDate()) + .map(Date::toString) + .orElse(null), + library.getRelatedArtifact()); + } + + public Page addPage(PlanDefinition sourceResource, PlanDefinition targetResource) + throws UnprocessableEntityException { + if (sourceResource != null + && targetResource != null + && !sourceResource.getUrl().equals(targetResource.getUrl())) { + throw new UnprocessableEntityException(URLS_DONT_MATCH); + } + var oldData = getPlanDefinitionChild(sourceResource); + var newData = getPlanDefinitionChild(targetResource); + var url = getPageUrl(sourceResource, targetResource); + var page = new Page<>(url, oldData, newData); + this.pages.add(page); + return page; + } + + private static PlanDefinitionChild getPlanDefinitionChild(PlanDefinition resource) { + return resource == null + ? null + : new PlanDefinitionChild( + resource.getTitle(), + resource.getIdPart(), + resource.getVersion(), + resource.getName(), + resource.getUrl()); + } + + public Page addPage(IBaseResource sourceResource, IBaseResource targetResource, String url) + throws UnprocessableEntityException { + var oldData = sourceResource == null + ? null + : new OtherChild( + null, + sourceResource.getIdElement().getIdPart(), + null, + null, + url, + sourceResource.fhirType()); + var newData = targetResource == null + ? null + : new OtherChild( + null, + targetResource.getIdElement().getIdPart(), + null, + null, + url, + targetResource.fhirType()); + var page = new Page<>(url, oldData, newData); + this.pages.add(page); + return page; + } + + public Optional getPage(String url) { + return this.pages.stream() + .filter(p -> p.url != null && p.url.equals(url)) + .findAny(); + } + + public void handleRelatedArtifacts() { + var manifest = this.getPage(this.manifestUrl); + if (manifest.isPresent()) { + var specLibrary = manifest.get(); + var manifestOldData = (LibraryChild) specLibrary.oldData; + var manifestNewData = (LibraryChild) specLibrary.newData; + if (manifestNewData != null) { + for (final var page : this.pages) { + if (page.oldData instanceof ValueSetChild oldValueSet) { + updateConditionsAndPriorities(manifestOldData, oldValueSet); + } + if (page.newData instanceof ValueSetChild newValueSet) { + updateConditionsAndPriorities(manifestNewData, newValueSet); + } + } + } + } + } + + private void updateConditionsAndPriorities(LibraryChild manifestData, ValueSetChild pageData) { + for (final var ra : manifestData.relatedArtifacts) { + pageData.leafValueSets.stream() + .filter(leafValueSet -> leafValueSet.memberOid != null + && leafValueSet.memberOid.equals(Canonicals.getIdPart(ra.getValue()))) + .forEach(leafValueSet -> { + updateConditions(ra, leafValueSet); + updatePriorities(ra, leafValueSet); + }); + } + } + + private void updateConditions(RelatedArtifactUrlWithOperation ra, ChangeLog.ValueSetChild.Leaf leafValueSet) { + ra.conditions.forEach(condition -> { + if (condition.value != null) { + var c = leafValueSet.tryAddCondition(condition.value); + c.operation = condition.operation; + } + }); + } + + private void updatePriorities(RelatedArtifactUrlWithOperation ra, ChangeLog.ValueSetChild.Leaf leafValueSet) { + if (ra.priority.value != null) { + var coding = ra.priority.value.getCodingFirstRep(); + leafValueSet.priority.value = coding.getCode(); + leafValueSet.priority.operation = ra.priority.operation; + } + } + + public static class Page { + private final T oldData; + private final T newData; + private String url; + private String resourceType; + + public T getOldData() { + return oldData; + } + + public T getNewData() { + return newData; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + Page(String url, T oldData, T newData) { + this.url = url; + this.oldData = oldData; + this.newData = newData; + if (oldData != null && oldData.getResourceType() != null) { + this.resourceType = oldData.getResourceType(); + } else if (newData != null && newData.getResourceType() != null) { + this.resourceType = newData.getResourceType(); + } + } + + public void addOperation(String type, String path, Object currentValue, Object originalValue) { + if (type != null) { + switch (type) { + case REPLACE -> addReplaceOperation(type, path, currentValue, originalValue); + case DELETE -> addDeleteOperation(type, path, originalValue); + case INSERT -> addInsertOperation(type, path, currentValue); + default -> + throw new UnprocessableEntityException( + "Unknown type provided when adding an operation to the ChangeLog"); + } + } else { + throw new UnprocessableEntityException( + "Type must be provided when adding an operation to the ChangeLog"); + } + } + + void addInsertOperation(String type, String path, Object currentValue) { + if (!type.equals(INSERT)) { + throw new UnprocessableEntityException(WRONG_TYPE); + } + this.newData.addOperation(type, path, currentValue, null); + } + + void addDeleteOperation(String type, String path, Object originalValue) { + if (!type.equals(DELETE)) { + throw new UnprocessableEntityException(WRONG_TYPE); + } + this.oldData.addOperation(type, path, null, originalValue); + } + + void addReplaceOperation(String type, String path, Object currentValue, Object originalValue) { + if (!type.equals(REPLACE)) { + throw new UnprocessableEntityException(WRONG_TYPE); + } + this.oldData.addOperation(type, path, currentValue, null); + this.newData.addOperation(type, path, null, originalValue); + } + } + + public static class ValueAndOperation { + private String value; + private Operation operation; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Operation getOperation() { + return operation; + } + + public void setOperation(Operation operation) { + if (operation != null) { + if (this.operation != null + && this.operation.type.equals(operation.type) + && this.operation.path.equals(operation.path) + && this.operation.newValue != operation.newValue) { + throw new UnprocessableEntityException("Multiple changes to the same element"); + } + this.operation = operation; + } + } + } + + public static class Operation { + private String type; + private String path; + private Object newValue; + private Object oldValue; + + Operation(String type, String path, Object newValue, Object originalValue) { + this.type = type; + this.path = path; + if (originalValue instanceof IPrimitiveType originalPrimitive) { + this.oldValue = originalPrimitive.getValue(); + } else if (originalValue instanceof IBase) { + this.oldValue = originalValue; + } else if (originalValue != null) { + this.oldValue = originalValue.toString(); + } + if (newValue instanceof IPrimitiveType newPrimitive) { + this.newValue = newPrimitive.getValue(); + } else if (newValue instanceof IBase) { + this.newValue = newValue; + } else if (newValue != null) { + this.newValue = newValue.toString(); + } + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Object getNewValue() { + return newValue; + } + + public Object getOldValue() { + return oldValue; + } + } + + public static class PageBase { + private final ValueAndOperation title = new ValueAndOperation(); + private final ValueAndOperation id = new ValueAndOperation(); + private final ValueAndOperation version = new ValueAndOperation(); + private final ValueAndOperation name = new ValueAndOperation(); + + public ValueAndOperation getTitle() { + return title; + } + + public ValueAndOperation getId() { + return id; + } + + public ValueAndOperation getVersion() { + return version; + } + + public ValueAndOperation getName() { + return name; + } + + public ValueAndOperation getUrl() { + return url; + } + + public String getResourceType() { + return resourceType; + } + + private final ValueAndOperation url = new ValueAndOperation(); + private final String resourceType; + + PageBase(String title, String id, String version, String name, String url, String resourceType) { + if (!StringUtils.isEmpty(title)) { + this.title.value = title; + } + if (!StringUtils.isEmpty(id)) { + this.id.value = id; + } + if (!StringUtils.isEmpty(version)) { + this.version.value = version; + } + if (!StringUtils.isEmpty(name)) { + this.name.value = name; + } + if (!StringUtils.isEmpty(url)) { + this.url.value = url; + } + this.resourceType = resourceType; + } + + public void addOperation(String type, String path, Object currentValue, Object originalValue) { + if (type != null) { + var newOp = new Operation(type, path, currentValue, originalValue); + if (path.equals("id")) { + this.id.setOperation(newOp); + } else if (path.contains("title")) { + this.title.setOperation(newOp); + } else if (path.equals("version")) { + this.version.setOperation(newOp); + } else if (path.equals("name")) { + this.name.setOperation(newOp); + } else if (path.equals("url")) { + this.url.setOperation(newOp); + } + } + } + } + + public static class ValueSetChild extends PageBase { + private final List codes = new ArrayList<>(); + private final List leafValueSets = new ArrayList<>(); + private final List operations = new ArrayList<>(); + private final ValueAndOperation priority = new ValueAndOperation(); + + public List getCodes() { + return codes; + } + + public List getLeafValueSets() { + return leafValueSets; + } + + public List getOperations() { + return operations; + } + + public ValueAndOperation getPriority() { + return priority; + } + + public static class Code { + private final String id; + private final String system; + private final String codeValue; + private final String version; + private final String display; + private final String memberOid; + private String codeSystemOid; + private String codeSystemName; + private final String parentValueSetName; + private final String parentValueSetTitle; + private final String parentValueSetUrl; + private Operation operation; + + public String getId() { + return id; + } + + public String getSystem() { + return system; + } + + public String getCodeValue() { + return codeValue; + } + + public String getVersion() { + return version; + } + + public String getDisplay() { + return display; + } + + public String getMemberOid() { + return memberOid; + } + + public String getCodeSystemOid() { + return codeSystemOid; + } + + public String getCodeSystemName() { + return codeSystemName; + } + + public String getParentValueSetName() { + return parentValueSetName; + } + + public String getParentValueSetTitle() { + return parentValueSetTitle; + } + + public String getParentValueSetUrl() { + return parentValueSetUrl; + } + + @SuppressWarnings("java:S107") + Code( + String id, + String system, + String code, + String version, + String display, + String memberOid, + String parentValueSetName, + String parentValueSetTitle, + String parentValueSetUrl, + Operation operation) { + this.id = id; + this.system = system; + if (system != null) { + this.codeSystemOid = getCodeSystemOid(system); + this.codeSystemName = getCodeSystemName(system); + } + this.codeValue = code; + this.version = version; + this.display = display; + this.memberOid = memberOid; + this.operation = operation; + this.parentValueSetName = parentValueSetName; + this.parentValueSetTitle = parentValueSetTitle; + this.parentValueSetUrl = parentValueSetUrl; + } + + public Code copy() { + return new Code( + this.id, + this.system, + this.codeValue, + this.version, + this.display, + this.memberOid, + this.parentValueSetName, + this.parentValueSetTitle, + this.parentValueSetUrl, + this.operation); + } + + public static String getCodeSystemOid(String systemUrl) { + if (systemUrl.contains("snomed")) { + return "2.16.840.1.113883.6.96"; + } else if (systemUrl.contains("icd-10")) { + return "2.16.840.1.113883.6.90"; + } else if (systemUrl.contains("icd-9")) { + return "2.16.840.1.113883.6.103, 2.16.840.1.113883.6.104"; + } else if (systemUrl.contains("loinc")) { + return "2.16.840.1.113883.6.1"; + } else { + return null; + } + } + + public static String getCodeSystemName(String systemUrl) { + if (systemUrl.contains("snomed")) { + return "SNOMEDCT"; + } else if (systemUrl.contains("icd-10")) { + return "ICD10CM"; + } else if (systemUrl.contains("icd-9")) { + return "ICD9CM"; + } else if (systemUrl.contains("loinc")) { + return "LOINC"; + } else { + return null; + } + } + + public Operation getOperation() { + return this.operation; + } + + public void setOperation(Operation operation) { + if (operation != null) { + if (this.operation != null + && this.operation.type.equals(operation.type) + && this.operation.path.equals(operation.path) + && this.operation.newValue != operation.newValue) { + throw new UnprocessableEntityException("Multiple changes to the same element"); + } + this.operation = operation; + } + } + } + + public static class Leaf { + private final String memberOid; + private final String name; + private final String title; + private final String url; + private List codeSystems = new ArrayList<>(); + private String status; + private List conditions = new ArrayList<>(); + private ValueAndOperation priority = new ValueAndOperation(); + private Operation operation; + + public String getMemberOid() { + return memberOid; + } + + public String getName() { + return name; + } + + public String getTitle() { + return title; + } + + public String getUrl() { + return url; + } + + public List getCodeSystems() { + return codeSystems; + } + + public String getStatus() { + return status; + } + + public List getConditions() { + return conditions; + } + + public ValueAndOperation getPriority() { + return priority; + } + + public Operation getOperation() { + return operation; + } + + public static class NameAndOid { + private final String name; + private final String oid; + + public String getName() { + return name; + } + + public String getOid() { + return oid; + } + + NameAndOid(String name, String oid) { + this.name = name; + this.oid = oid; + } + + public NameAndOid copy() { + return new NameAndOid(this.name, this.oid); + } + } + + Leaf(String memberOid, String name, String title, String url, PublicationStatus status) { + this.memberOid = memberOid; + this.name = name; + this.title = title; + this.url = url; + if (status != null) { + this.status = status.getDisplay(); + } + } + + public Leaf copy() { + var copy = new Leaf(this.memberOid, this.name, this.title, this.url, null); + copy.status = this.status; + copy.codeSystems = + this.codeSystems.stream().map(NameAndOid::copy).collect(Collectors.toList()); + copy.conditions = this.conditions.stream().map(Code::copy).collect(Collectors.toList()); + copy.priority = new ValueAndOperation(); + copy.priority.value = this.priority.value; + copy.priority.operation = this.priority.operation; + copy.operation = this.operation; + return copy; + } + + public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { + var coding = condition.getCodingFirstRep(); + var conditionName = + (coding.getDisplay() == null || coding.getDisplay().isBlank()) + ? condition.getText() + : coding.getDisplay(); + final var maybeExisting = this.conditions.stream() + .filter(code -> + code.system.equals(coding.getSystem()) && code.codeValue.equals(coding.getCode())) + .findAny(); + if (maybeExisting.isEmpty()) { + final var newCondition = new ValueSetChild.Code( + coding.getId(), + coding.getSystem(), + coding.getCode(), + coding.getVersion(), + conditionName, + null, + null, + null, + null, + null); + this.conditions.add(newCondition); + return newCondition; + } else { + return maybeExisting.get(); + } + } + } + + @SuppressWarnings("java:S107") + ValueSetChild( + String title, + String id, + String version, + String name, + String url, + List compose, + List contains, + Map codeMap, + Map> leafMetadataMap, + String priority) { + super(title, id, version, name, url, "ValueSet"); + if (contains != null) { + contains.forEach(contained -> { + if (contained.getCode() != null && codeMap.containsKey(contained.getCode())) { + this.codes.add(codeMap.get(contained.getCode())); + } + }); + } + if (compose != null) { + compose.stream() + .filter(ConceptSetComponent::hasValueSet) + .flatMap(c -> c.getValueSet().stream()) + .filter(PrimitiveType::hasValue) + .map(PrimitiveType::getValue) + .forEach(vs -> { + // sometimes the value set reference is unversioned - implying that the latest version + // should be used + // we need to make sure the diff operation only has the latest version in it, thereby we + // can get away with just having one url in the map and taking it + var urlPart = Canonicals.getUrl(vs); + if (Canonicals.getVersion(vs) == null) { + // assume there is only the latest version + var latest = leafMetadataMap + .get(urlPart) + .entrySet() + .iterator() + .next() + .getValue(); + // creating a new object because modifying it causes weirdness later + leafValueSets.add(latest.copy()); + } else { + var versionPart = Canonicals.getVersion(vs); + var leaf = leafMetadataMap.get(urlPart).get(versionPart); + // creating a new object because modifying it causes weirdness later + leafValueSets.add(leaf.copy()); + } + }); + } + if (priority != null) { + this.priority.value = priority; + } + } + + @Override + public void addOperation(String type, String path, Object newValue, Object originalValue) { + if (type != null) { + super.addOperation(type, path, newValue, originalValue); + var operation = new Operation(type, path, newValue, originalValue); + if (path.contains("compose")) { + addOperationHandleCompose(type, path, newValue, originalValue, operation); + } else if (path.contains("expansion")) { + addOperationHandleExpansion(type, path, newValue, originalValue, operation); + } else if (path.contains("useContext")) { + addOperationHandleUseContext(newValue, originalValue, operation); + } else { + this.operations.add(operation); + } + } + } + + @SuppressWarnings("unchecked") + private void addOperationHandleCompose( + String type, String path, Object newValue, Object originalValue, Operation operation) { + // if the valuesets changed + List urlsToCheck = List.of(); + // default to the original operation for use with primitive types + List updatedOperations = List.of(operation); + if (newValue instanceof IPrimitiveType && ((IPrimitiveType) newValue).hasValue()) { + urlsToCheck = List.of(((IPrimitiveType) newValue).getValue()); + } else if (originalValue instanceof IPrimitiveType + && ((IPrimitiveType) originalValue).hasValue()) { + urlsToCheck = List.of(((IPrimitiveType) originalValue).getValue()); + } else if (newValue instanceof ValueSet.ValueSetComposeComponent newVSCC + && newVSCC.getIncludeFirstRep().hasValueSet()) { + urlsToCheck = newVSCC.getInclude().stream() + .filter(ConceptSetComponent::hasValueSet) + .flatMap(include -> include.getValueSet().stream()) + .filter(PrimitiveType::hasValue) + .map(PrimitiveType::getValue) + .toList(); + updatedOperations = urlsToCheck.stream() + .map(url -> new Operation(type, path, url, type.equals(REPLACE) ? originalValue : null)) + .toList(); + } else if (originalValue instanceof ValueSet.ValueSetComposeComponent originalVSCC + && originalVSCC.getIncludeFirstRep().hasValueSet()) { + urlsToCheck = originalVSCC.getInclude().stream() + .filter(ConceptSetComponent::hasValueSet) + .flatMap(include -> include.getValueSet().stream()) + .filter(PrimitiveType::hasValue) + .map(PrimitiveType::getValue) + .toList(); + updatedOperations = urlsToCheck.stream() + .map(url -> new Operation(type, path, type.equals(REPLACE) ? newValue : null, url)) + .toList(); + } + handleUrlsToCheck(urlsToCheck, updatedOperations); + } + + private void handleUrlsToCheck(List urlsToCheck, List updatedOperations) { + if (!urlsToCheck.isEmpty()) { + for (var i = 0; i < urlsToCheck.size(); i++) { + final var urlNotNull = Canonicals.getIdPart(urlsToCheck.get(i)); + for (final var leafValueSet : this.leafValueSets) { + if (leafValueSet.memberOid.equals(urlNotNull)) { + leafValueSet.operation = updatedOperations.get(i); + } + } + } + } + } + + private void addOperationHandleExpansion( + String type, String path, Object newValue, Object originalValue, Operation operation) { + if (path.contains("expansion.contains[")) { + // if the codes themselves changed + String codeToCheck = getCodeToCheck(newValue, originalValue); + updateCodeOperation(codeToCheck, operation); + } else if (newValue instanceof ValueSet.ValueSetExpansionComponent + || originalValue instanceof ValueSet.ValueSetExpansionComponent) { + var contains = newValue instanceof ValueSet.ValueSetExpansionComponent newVSEC + ? newVSEC + : (ValueSet.ValueSetExpansionComponent) originalValue; + contains.getContains().forEach(c -> { + Operation updatedOperation; + if (newValue instanceof ValueSet.ValueSetExpansionComponent) { + updatedOperation = new Operation(type, path, c.getCode(), null); + } else { + updatedOperation = new Operation(type, path, null, c.getCode()); + } + updateCodeOperation(c.getCode(), updatedOperation); + }); + } + } + + @SuppressWarnings("unchecked") + private static String getCodeToCheck(Object newValue, Object originalValue) { + String codeToCheck = null; + if (newValue instanceof IPrimitiveType || originalValue instanceof IPrimitiveType) { + codeToCheck = newValue instanceof IPrimitiveType + ? ((IPrimitiveType) newValue).getValue() + : ((IPrimitiveType) originalValue).getValue(); + } else if (originalValue instanceof ValueSet.ValueSetExpansionContainsComponent originalVSECC) { + codeToCheck = originalVSECC.getCode(); + } + return codeToCheck; + } + + private void addOperationHandleUseContext(Object newValue, Object originalValue, Operation operation) { + String priorityToCheck = null; + if (newValue instanceof UsageContext newUseContext + && newUseContext.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) + && newUseContext.getCode().getCode().equals(TransformProperties.VSM_PRIORITY_CODE)) { + priorityToCheck = newUseContext + .getValueCodeableConcept() + .getCodingFirstRep() + .getCode(); + } else if (originalValue instanceof UsageContext originalUseContext + && originalUseContext.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) + && originalUseContext.getCode().getCode().equals(TransformProperties.VSM_PRIORITY_CODE)) { + priorityToCheck = originalUseContext + .getValueCodeableConcept() + .getCodingFirstRep() + .getCode(); + } + if (priorityToCheck != null) { + this.priority.operation = operation; + } + } + + private void updateCodeOperation(String codeToCheck, Operation operation) { + if (codeToCheck != null) { + final String codeNotNull = codeToCheck; + this.codes.stream() + .filter(code -> code.codeValue != null) + .filter(code -> code.codeValue.equals(codeNotNull)) + .findAny() + .ifPresentOrElse( + code -> code.setOperation(operation), + () -> + // drop unmatched operations in the base operations list + this.operations.add(operation)); + } + } + } + + public static class PlanDefinitionChild extends PageBase { + PlanDefinitionChild(String title, String id, String version, String name, String url) { + super(title, id, version, name, url, "PlanDefinition"); + } + } + + public static class OtherChild extends PageBase { + OtherChild(String title, String id, String version, String name, String url, String fhirType) { + super(title, id, version, name, url, fhirType); + } + } + + public static class RelatedArtifactUrlWithOperation extends ValueAndOperation { + private final RelatedArtifact fullRelatedArtifact; + private List conditions = new ArrayList<>(); + private final CodeableConceptWithOperation priority = new CodeableConceptWithOperation(null); + + public RelatedArtifact getFullRelatedArtifact() { + return fullRelatedArtifact; + } + + public List getConditions() { + return conditions; + } + + public CodeableConceptWithOperation getPriority() { + return priority; + } + + public static class CodeableConceptWithOperation { + private CodeableConcept value; + private Operation operation; + + CodeableConceptWithOperation(CodeableConcept e) { + this.value = e; + } + + public CodeableConcept getValue() { + return value; + } + + public Operation getOperation() { + return operation; + } + } + + RelatedArtifactUrlWithOperation(RelatedArtifact relatedArtifact) { + if (relatedArtifact != null) { + this.setValue(relatedArtifact.getResource()); + this.conditions = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmCondition).stream() + .map(e -> new CodeableConceptWithOperation((CodeableConcept) e.getValue())) + .toList(); + var priorities = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmPriority).stream() + .map(e -> (CodeableConcept) e.getValue()) + .toList(); + if (priorities.size() > 1) { + throw new UnprocessableEntityException("too many priorities"); + } else if (priorities.size() == 1) { + this.priority.value = priorities.get(0); + } else { + this.priority.value = new CodeableConcept( + new Coding(TransformProperties.usPHUsageContext, "routine", "Routine")); + } + } + this.fullRelatedArtifact = relatedArtifact; + } + } + + public static class LibraryChild extends PageBase { + private final ValueAndOperation purpose = new ValueAndOperation(); + private final ValueAndOperation effectiveStart = new ValueAndOperation(); + private final ValueAndOperation releaseDate = new ValueAndOperation(); + private final List relatedArtifacts = new ArrayList<>(); + + @SuppressWarnings("java:S107") + LibraryChild( + String name, + String purpose, + String title, + String id, + String version, + String url, + String effectiveStart, + String releaseDate, + List relatedArtifacts) { + super(title, id, version, name, url, "Library"); + if (!StringUtils.isEmpty(purpose)) { + this.purpose.value = purpose; + } + if (!StringUtils.isEmpty(effectiveStart)) { + this.effectiveStart.value = effectiveStart; + } + if (!StringUtils.isEmpty(releaseDate)) { + this.releaseDate.value = releaseDate; + } + if (!relatedArtifacts.isEmpty()) { + relatedArtifacts.forEach(ra -> this.relatedArtifacts.add(new RelatedArtifactUrlWithOperation(ra))); + } + } + + public ValueAndOperation getPurpose() { + return purpose; + } + + public ValueAndOperation getEffectiveStart() { + return effectiveStart; + } + + public ValueAndOperation getReleaseDate() { + return releaseDate; + } + + public List getRelatedArtifacts() { + return relatedArtifacts; + } + + private Optional getRelatedArtifactFromUrl(String target) { + return this.relatedArtifacts.stream() + .filter(ra -> ra.getValue() != null && ra.getValue().equals(target)) + .findAny(); + } + + private void tryAddConditionOperation( + Extension maybeCondition, RelatedArtifactUrlWithOperation target, Operation newOperation) { + if (maybeCondition.getUrl().equals(TransformProperties.vsmCondition)) { + target.conditions.stream() + .filter(e -> e.value + .getCodingFirstRep() + .getSystem() + .equals(((CodeableConcept) maybeCondition.getValue()) + .getCodingFirstRep() + .getSystem()) + && e.value + .getCodingFirstRep() + .getCode() + .equals(((CodeableConcept) maybeCondition.getValue()) + .getCodingFirstRep() + .getCode())) + .findAny() + .ifPresent(condition -> condition.operation = newOperation); + } + } + + private void tryAddPriorityOperation( + Extension maybePriority, RelatedArtifactUrlWithOperation target, Operation newOperation) { + if (maybePriority.getUrl().equals(TransformProperties.vsmPriority) + && (target.priority.value != null + && target.priority + .value + .getCodingFirstRep() + .getSystem() + .equals(((CodeableConcept) maybePriority.getValue()) + .getCodingFirstRep() + .getSystem()) + && target.priority + .value + .getCodingFirstRep() + .getCode() + .equals(((CodeableConcept) maybePriority.getValue()) + .getCodingFirstRep() + .getCode()))) { + // priority will always be replace because: + // insert = an extension exists where it did not before, which is a replacement from "routine" + // to "emergent" + // delete = an extension does not exist where it did before, which is a replacement from + // "emergent" to "routine" + newOperation.type = REPLACE; + target.priority.operation = newOperation; + } + } + + @Override + public void addOperation(String type, String path, Object currentValue, Object originalValue) { + if (type != null) { + super.addOperation(type, path, currentValue, originalValue); + var newOperation = new Operation(type, path, currentValue, originalValue); + if (path != null && path.contains("elatedArtifact")) { + addOperationHandleRelatedArtifacts(path, currentValue, originalValue, newOperation); + } else if (path != null && path.equals("name")) { + this.getName().setOperation(newOperation); + } else if (path != null && path.contains("purpose")) { + this.purpose.setOperation(newOperation); + } else if (path != null && path.equals("approvalDate")) { + this.releaseDate.setOperation(newOperation); + } else if (path != null && path.contains("effectivePeriod")) { + this.effectiveStart.setOperation(newOperation); + } + } + } + + private void addOperationHandleRelatedArtifacts( + String path, Object currentValue, Object originalValue, Operation newOperation) { + Optional operationTarget = Optional.empty(); + if (currentValue instanceof RelatedArtifact currentRelatedArtifact) { + operationTarget = getRelatedArtifactFromUrl(currentRelatedArtifact.getResource()); + } else if (originalValue instanceof RelatedArtifact originalRelatedArtifact) { + operationTarget = getRelatedArtifactFromUrl(originalRelatedArtifact.getResource()); + } else if (path.contains("[")) { + var matcher = Pattern.compile("relatedArtifact\\[(\\d+)]").matcher(path); + if (matcher.find()) { + var relatedArtifactIndex = Integer.parseInt(matcher.group(1)); + operationTarget = Optional.of(this.relatedArtifacts.get(relatedArtifactIndex)); + } + } + if (operationTarget.isPresent()) { + if (path.contains("xtension[")) { + var matcher = Pattern.compile("xtension\\[(\\d+)]").matcher(path); + if (matcher.find()) { + var extension = operationTarget + .get() + .fullRelatedArtifact + .getExtension() + .get(Integer.parseInt(matcher.group(1))); + tryAddConditionOperation(extension, operationTarget.orElse(null), newOperation); + tryAddPriorityOperation(extension, operationTarget.orElse(null), newOperation); + } + } else if (currentValue instanceof Extension currentExtension) { + tryAddConditionOperation(currentExtension, operationTarget.orElse(null), newOperation); + tryAddPriorityOperation(currentExtension, operationTarget.orElse(null), newOperation); + } else if (originalValue instanceof Extension originalExtension) { + tryAddConditionOperation(originalExtension, operationTarget.orElse(null), newOperation); + tryAddPriorityOperation(originalExtension, operationTarget.orElse(null), newOperation); + } else { + operationTarget.get().setOperation(newOperation); + } + } + } + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ICreateChangelogProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ICreateChangelogProcessor.java new file mode 100644 index 0000000000..62b343b473 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ICreateChangelogProcessor.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.fhir.cr.common; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +public interface ICreateChangelogProcessor extends IOperationProcessor { + + IBaseResource createChangelog(IBaseResource source, IBaseResource target, IBaseResource terminologyEndpoint); +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/TransformProperties.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/TransformProperties.java index 42478d6f36..fcf24ed108 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/TransformProperties.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/TransformProperties.java @@ -25,6 +25,7 @@ private TransformProperties() {} public static final String crmiIsOwned = "http://hl7.org/fhir/StructureDefinition/artifact-isOwned"; public static final String vsmCondition = "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition"; public static final String vsmPriority = "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority"; + public static final String VSM_PRIORITY_CODE = "priority"; public static final String CRMI_INTENDED_USAGE_CONTEXT_EXT_URL = "http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-intendedUsageContext"; public static final String authoritativeSourceExtUrl = diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java index 2e0c99bdab..767669c6ae 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java @@ -19,9 +19,11 @@ import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.CrSettings; import org.opencds.cqf.fhir.cr.common.ArtifactDiffProcessor; +import org.opencds.cqf.fhir.cr.common.CreateChangelogProcessor; import org.opencds.cqf.fhir.cr.common.DataRequirementsProcessor; import org.opencds.cqf.fhir.cr.common.DeleteProcessor; import org.opencds.cqf.fhir.cr.common.IArtifactDiffProcessor; +import org.opencds.cqf.fhir.cr.common.ICreateChangelogProcessor; import org.opencds.cqf.fhir.cr.common.IDataRequirementsProcessor; import org.opencds.cqf.fhir.cr.common.IDeleteProcessor; import org.opencds.cqf.fhir.cr.common.IOperationProcessor; @@ -56,6 +58,7 @@ public class LibraryProcessor { protected IWithdrawProcessor withdrawProcessor; protected IReviseProcessor reviseProcessor; protected IArtifactDiffProcessor artifactDiffProcessor; + protected ICreateChangelogProcessor createChangelogProcessor; protected IRepository repository; protected CrSettings crSettings; @@ -100,6 +103,9 @@ public LibraryProcessor( if (p instanceof IArtifactDiffProcessor artifactDiff) { artifactDiffProcessor = artifactDiff; } + if (p instanceof ICreateChangelogProcessor createChangelog) { + createChangelogProcessor = createChangelog; + } }); } } @@ -285,4 +291,13 @@ public , R extends IBaseResource> IBaseParamete null, terminologyEndpoint); } + + public , R extends IBaseResource> IBaseResource createChangelog( + Either3 sourceLibrary, + Either3 targetLibrary, + IBaseResource terminologyEndpoint) { + var processor = createChangelogProcessor != null ? createChangelogProcessor : new CreateChangelogProcessor(); + return processor.createChangelog( + resolveLibrary(sourceLibrary), resolveLibrary(targetLibrary), terminologyEndpoint); + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherDSTU3.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherDSTU3.java index 41c4c28a61..8089a5f73a 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherDSTU3.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherDSTU3.java @@ -8,9 +8,9 @@ import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.TokenParam; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.apache.commons.lang3.NotImplementedException; import org.hl7.fhir.dstu3.model.CodeType; @@ -24,8 +24,8 @@ public class ResourceMatcherDSTU3 implements ResourceMatcher { - private Map pathCache = new HashMap<>(); - private Map searchParams = new HashMap<>(); + private Map pathCache = new ConcurrentHashMap<>(); + private Map searchParams = new ConcurrentHashMap<>(); @Override public IFhirPath getEngine() { diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR4.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR4.java index 085bfe4906..5b686afefe 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR4.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR4.java @@ -8,9 +8,9 @@ import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.TokenParam; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.apache.commons.lang3.NotImplementedException; import org.hl7.fhir.instance.model.api.IBase; @@ -28,8 +28,8 @@ public class ResourceMatcherR4 implements ResourceMatcher { - private Map pathCache = new HashMap<>(); - private Map searchParams = new HashMap<>(); + private Map pathCache = new ConcurrentHashMap<>(); + private Map searchParams = new ConcurrentHashMap<>(); @Override public IFhirPath getEngine() { diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR5.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR5.java index 7772e37887..bd1cf78b9e 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR5.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherR5.java @@ -8,9 +8,9 @@ import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.TokenParam; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.apache.commons.lang3.NotImplementedException; import org.hl7.fhir.instance.model.api.IBase; @@ -24,8 +24,8 @@ public class ResourceMatcherR5 implements ResourceMatcher { - private Map pathCache = new HashMap<>(); - private Map searchParams = new HashMap<>(); + private Map pathCache = new ConcurrentHashMap<>(); + private Map searchParams = new ConcurrentHashMap<>(); @Override public IFhirPath getEngine() {