From 0ce7430b4a65fc8519b9c6841c8890ffd1abd5c6 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Mon, 2 Feb 2026 12:19:01 -0800 Subject: [PATCH 01/22] Initial commit - changelog operation is working Clean up pending --- .../common/HapiCreateChangelogProcessor.java | 255 ++++ .../cr/hapi/config/CrProcessorConfig.java | 6 +- .../fhir/cr/hapi/config/r4/CrR4Config.java | 3 +- .../r4/CreateChangelogOperationConfig.java | 35 + .../LibraryCreateChangelogProvider.java | 48 + .../cr/common/CreateChangelogProcessor.java | 1155 +++++++++++++++++ .../cr/common/ICreateChangelogProcessor.java | 9 + .../cqf/fhir/cr/library/LibraryProcessor.java | 13 + 8 files changed, 1522 insertions(+), 2 deletions(-) create mode 100644 cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessor.java create mode 100644 cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CreateChangelogOperationConfig.java create mode 100644 cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/library/LibraryCreateChangelogProvider.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/CreateChangelogProcessor.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ICreateChangelogProcessor.java 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..228d1ed077 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessor.java @@ -0,0 +1,255 @@ +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.Endpoint; +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.ValueSet; +import org.opencds.cqf.fhir.cr.common.ArtifactDiffProcessor.DiffCache; +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 IRepository repository; + private final FhirVersionEnum fhirVersion; + private final PackageProcessor packageProcessor; + + private final HapiArtifactDiffProcessor hapiArtifactDiffProcessor; + + public HapiCreateChangelogProcessor(IRepository repository) { + this.repository = 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, Endpoint 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(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) { + service.shutdownNow(); + throw new UnprocessableEntityException(e.getMessage()); + } + + // 2) Fill the cache with the bundle contents + var cache = new DiffCache(); + Optional sourceResource = Optional.empty(); + Optional targetResource = Optional.empty(); + 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())) { + sourceResource = Optional.of((Library) 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())) { + targetResource = Optional.of((Library) metadataResource); + } + } + } + + // 3) Use cached resources to create diff and changelog + var targetAdapter = IAdapterFactory.forFhirVersion(FhirVersionEnum.R4) + .createKnowledgeArtifactAdapter(targetResource.orElse(null)); + var diffParameters = hapiArtifactDiffProcessor.getArtifactDiff( + sourceResource.orElse(null), targetResource.orElse(null), 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; + } + + 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 resources = cache.getResourcesForUrl(url); + var resourceType = Canonicals.getResourceType(url); + // Check if the resource pair was already processed + var wasPageAlreadyProcessed = changelog.getPage(url).isPresent(); + if (!resources.isEmpty() && !wasPageAlreadyProcessed) { + final MetadataResource sourceResource = resources.get(0).isSource + ? resources.get(0).resource + : (resources.size() > 1 ? resources.get(1).resource : null); + final MetadataResource targetResource = resources.get(0).isSource + ? (resources.size() > 1 ? resources.get(1).resource : null) + : resources.get(0).resource; + // 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); + }); + for (var change : changes) { + 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")) { + // 3) 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")) { + originalValue = + Optional.of((new BeanWrapperImpl(sourceResource).getPropertyValue(path.get()))); + } + } catch (Exception e) { + // TODO: handle exception + // var message = e.getMessage(); + throw new InternalErrorException("Could not process path: " + path + ": " + e.getMessage()); + } + + // 4) Add a new operation to the ChangeLog + page.addOperation( + type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null), changelog); + } + } + } + } + + 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 56a531650a..c05f00db72 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; @@ -71,7 +72,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 f03be6bf0e..72315ecbe0 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 @@ -62,7 +62,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/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..8c53e65018 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/CreateChangelogProcessor.java @@ -0,0 +1,1155 @@ +package org.opencds.cqf.fhir.cr.common; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import java.util.ArrayList; +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.Endpoint; +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.Parameters; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.PlanDefinition; +import org.hl7.fhir.r4.model.RelatedArtifact; +import org.hl7.fhir.r4.model.UsageContext; +import org.hl7.fhir.r4.model.ValueSet; +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; + +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, Endpoint terminologyEndpoint) { + logger.info("Unable to perform $create-changelog outside of HAPI context"); + return new Parameters(); + } + + public static class ChangeLog { + public List> pages; + public String manifestUrl; + + public ChangeLog(String url) { + this.pages = new ArrayList>(); + this.manifestUrl = url; + } + + public Page addPage(String url, T oldData, T newData) { + var page = new Page(url, oldData, newData); + this.pages.add(page); + return page; + } + + 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 don't 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 = sourceResource == null ? targetResource.getUrl() : sourceResource.getUrl(); + var page = new Page(url, oldData, newData); + this.pages.add(page); + return page; + } + + private Optional getPriority(ValueSet valueSet) { + return valueSet.getUseContext().stream() + .filter(uc -> uc.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) + && uc.getCode().getCode().equals("priority")) + .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()) { + valueSet.getCompose().getInclude().forEach(concept -> { + if (concept.hasConcept()) { + var codeSystemName = ValueSetChild.Code.getCodeSystemName(concept.getSystem()); + var codeSystemOid = ValueSetChild.Code.getCodeSystemOid(concept.getSystem()); + 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)); + } + 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); + }); + } + }); + } + if (valueSet.getExpansion().hasContains()) { + valueSet.getExpansion().getContains().forEach((cnt) -> { + if (!codeMap.containsKey(cnt.getCode())) { + var codeSystemName = ValueSetChild.Code.getCodeSystemName(cnt.getSystem()); + var codeSystemOid = ValueSetChild.Code.getCodeSystemOid(cnt.getSystem()); + 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)); + } + mapExpansionContainsToCodeMap( + codeMap, + cnt, + Canonicals.getIdPart(valueSet.getUrl()), + valueSet.getName(), + valueSet.getTitle(), + valueSet.getUrl()); + } + }); + } + } + } + + 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 don't match"); + } + var oldData = sourceResource == null + ? null + : new LibraryChild( + sourceResource.getName(), + sourceResource.getPurpose(), + sourceResource.getTitle(), + sourceResource.getIdPart(), + sourceResource.getVersion(), + sourceResource.getUrl(), + Optional.ofNullable((Period) sourceResource.getEffectivePeriod()) + .map(p -> p.getStart()) + .map(s -> s.toString()) + .orElse(null), + Optional.ofNullable(sourceResource.getApprovalDate()) + .map(s -> s.toString()) + .orElse(null), + sourceResource.getRelatedArtifact()); + var newData = targetResource == null + ? null + : new LibraryChild( + targetResource.getName(), + targetResource.getPurpose(), + targetResource.getTitle(), + targetResource.getIdPart(), + targetResource.getVersion(), + targetResource.getUrl(), + Optional.ofNullable((Period) targetResource.getEffectivePeriod()) + .map(p -> p.getStart()) + .map(s -> s.toString()) + .orElse(null), + Optional.ofNullable(targetResource.getApprovalDate()) + .map(s -> s.toString()) + .orElse(null), + targetResource.getRelatedArtifact()); + var url = sourceResource == null ? targetResource.getUrl() : sourceResource.getUrl(); + var page = new Page(url, oldData, newData); + this.pages.add(page); + return page; + } + + public Page addPage(PlanDefinition sourceResource, PlanDefinition targetResource) + throws UnprocessableEntityException { + if (sourceResource != null + && targetResource != null + && !sourceResource.getUrl().equals(targetResource.getUrl())) { + throw new UnprocessableEntityException("URLs don't match"); + } + var oldData = sourceResource == null + ? null + : new PlanDefinitionChild( + sourceResource.getTitle(), + sourceResource.getIdPart(), + sourceResource.getVersion(), + sourceResource.getName(), + sourceResource.getUrl()); + var newData = targetResource == null + ? null + : new PlanDefinitionChild( + targetResource.getTitle(), + targetResource.getIdPart(), + targetResource.getVersion(), + targetResource.getName(), + targetResource.getUrl()); + var url = sourceResource == null ? targetResource.getUrl() : sourceResource.getUrl(); + var page = new Page(url, oldData, newData); + this.pages.add(page); + return page; + } + + 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) { + for (final var ra : manifestOldData.relatedArtifacts) { + ((ValueSetChild) page.oldData) + .leafValuesets.stream() + .filter(leafValueSet -> leafValueSet.memberOid != null + && leafValueSet.memberOid.equals( + Canonicals.getIdPart(ra.value))) + .forEach(leafValueSet -> { + updateConditions(ra, leafValueSet); + updatePriorities(ra, leafValueSet); + }); + } + } + if (page.newData instanceof ValueSetChild) { + for (final var ra : manifestNewData.relatedArtifacts) { + ((ValueSetChild) page.newData) + .leafValuesets.stream() + .filter(leafValueSet -> leafValueSet.memberOid != null + && leafValueSet.memberOid.equals( + Canonicals.getIdPart(ra.value))) + .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 { + public T oldData; + public T newData; + public String url; + public String resourceType; + + Page(String url, T oldData, T newData) { + this.url = url; + this.oldData = oldData; + this.newData = newData; + if (oldData != null && oldData.resourceType != null) { + this.resourceType = oldData.resourceType; + } else if (newData != null && newData.resourceType != null) { + this.resourceType = newData.resourceType; + } + } + + public void addOperation( + String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { + if (type != null) { + switch (type) { + case "replace": + addReplaceOperation(type, path, currentValue, originalValue, parent); + break; + case "delete": + addDeleteOperation(type, path, null, originalValue, parent); + break; + case "insert": + addInsertOperation(type, path, currentValue, null, parent); + break; + 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, Object originalValue, ChangeLog parent) { + if (type != "insert") { + throw new UnprocessableEntityException("wrong type"); + } + this.newData.addOperation(type, path, currentValue, originalValue, parent); + } + + void addDeleteOperation( + String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { + if (type != "delete") { + throw new UnprocessableEntityException("wrong type"); + } + this.oldData.addOperation(type, path, currentValue, originalValue, parent); + } + + void addReplaceOperation( + String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { + if (type != "replace") { + throw new UnprocessableEntityException("wrong type"); + } + this.oldData.addOperation(type, path, currentValue, null, parent); + this.newData.addOperation(type, path, null, originalValue, parent); + } + } + + public static class ValueAndOperation { + public String value; + public Operation operation; + + public void setOperation(Operation operation) { + if (operation != null) { + if (this.operation != null + && this.operation.type == operation.type + && this.operation.path == operation.path + && this.operation.newValue != operation.newValue) { + throw new UnprocessableEntityException("Multiple changes to the same element"); + } + this.operation = operation; + } + } + } + + public static class Operation { + public String type; + public String path; + public Object newValue; + public Object oldValue; + + Operation(String type, String path, IBase newValue, IBase original) { + this.type = type; + this.path = path; + this.oldValue = original; + this.newValue = newValue; + } + + Operation(String type, String path, Object newValue, Object originalValue) { + this.type = type; + this.path = path; + if (originalValue instanceof IPrimitiveType) { + this.oldValue = ((IPrimitiveType) originalValue).getValue(); + } else if (originalValue instanceof IBase) { + this.oldValue = originalValue; + } else if (originalValue != null) { + this.oldValue = originalValue.toString(); + } + if (newValue instanceof IPrimitiveType) { + this.newValue = ((IPrimitiveType) newValue).getValue(); + } else if (newValue instanceof IBase) { + this.newValue = newValue; + } else if (newValue != null) { + this.newValue = newValue.toString(); + } + } + } + + public static class PageBase { + public ValueAndOperation title = new ValueAndOperation(); + public ValueAndOperation id = new ValueAndOperation(); + public ValueAndOperation version = new ValueAndOperation(); + public ValueAndOperation name = new ValueAndOperation(); + public ValueAndOperation url = new ValueAndOperation(); + public 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, ChangeLog parent) { + 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 { + public List codes = new ArrayList<>(); + public List leafValuesets = new ArrayList<>(); + public List operations = new ArrayList<>(); + public ValueAndOperation priority = new ValueAndOperation(); + + public static class Code { + public String id; + public String system; + public String code; + public String version; + public String display; + public String memberOid; + public String codeSystemOid; + public String codeSystemName; + public String parentValueSetName; + public String parentValueSetTitle; + public String parentValueSetUrl; + public Operation operation; + + 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.code = 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.code, + 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 == operation.type + && this.operation.path == operation.path + && this.operation.newValue != operation.newValue) { + throw new UnprocessableEntityException("Multiple changes to the same element"); + } + this.operation = operation; + } + } + } + + public static class Leaf { + public String memberOid; + public String name; + public String title; + public String url; + public List codeSystems = new ArrayList(); + public String status; + public List conditions = new ArrayList(); + public ValueAndOperation priority = new ValueAndOperation(); + public Operation operation; + + public static class NameAndOid { + public String name; + public String 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(c -> c.copy()).collect(Collectors.toList()); + copy.conditions = + this.conditions.stream().map(c -> c.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.code.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(); + } + } + } + + 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(cmp -> cmp.hasValueSet()) + .flatMap(c -> c.getValueSet().stream()) + .filter(vs -> vs.hasValue()) + .map(vs -> vs.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, ChangeLog parent) { + if (type != null) { + super.addOperation(type, path, newValue, originalValue, parent); + var operation = new Operation(type, path, newValue, originalValue); + if (path.contains("compose")) { + // 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 + && ((ValueSet.ValueSetComposeComponent) newValue) + .getIncludeFirstRep() + .hasValueSet()) { + urlsToCheck = ((ValueSet.ValueSetComposeComponent) newValue) + .getInclude().stream() + .filter(include -> include.hasValueSet()) + .flatMap(include -> include.getValueSet().stream()) + .filter(canonical -> canonical.hasValue()) + .map(canonical -> canonical.getValue()) + .collect(Collectors.toList()); + updatedOperations = urlsToCheck.stream() + .map(url -> new Operation( + type, path, url, type.equals("replace") ? originalValue : null)) + .collect(Collectors.toList()); + } else if (originalValue instanceof ValueSet.ValueSetComposeComponent + && ((ValueSet.ValueSetComposeComponent) originalValue) + .getIncludeFirstRep() + .hasValueSet()) { + urlsToCheck = ((ValueSet.ValueSetComposeComponent) originalValue) + .getInclude().stream() + .filter(include -> include.hasValueSet()) + .flatMap(include -> include.getValueSet().stream()) + .filter(canonical -> canonical.hasValue()) + .map(canonical -> canonical.getValue()) + .collect(Collectors.toList()); + updatedOperations = urlsToCheck.stream() + .map(url -> + new Operation(type, path, type.equals("replace") ? newValue : null, url)) + .collect(Collectors.toList()); + } + 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); + } + } + } + } + } else if (path.contains("expansion")) { + if (path.contains("expansion.contains[")) { + // if the codes themselves changed + 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) { + codeToCheck = ((ValueSet.ValueSetExpansionContainsComponent) originalValue).getCode(); + } + updateCodeOperation(codeToCheck, operation); + } else if (newValue instanceof ValueSet.ValueSetExpansionComponent + || originalValue instanceof ValueSet.ValueSetExpansionComponent) { + var contains = newValue instanceof ValueSet.ValueSetExpansionComponent + ? (ValueSet.ValueSetExpansionComponent) newValue + : (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); + }); + } + } else if (path.contains("useContext")) { + String priorityToCheck = null; + if (newValue instanceof UsageContext + && ((UsageContext) newValue) + .getCode() + .getSystem() + .equals(TransformProperties.usPHUsageContextType) + && ((UsageContext) newValue).getCode().getCode().equals("priority")) { + priorityToCheck = ((UsageContext) newValue) + .getValueCodeableConcept() + .getCodingFirstRep() + .getCode(); + } else if (originalValue instanceof UsageContext + && ((UsageContext) originalValue) + .getCode() + .getSystem() + .equals(TransformProperties.usPHUsageContextType) + && ((UsageContext) originalValue) + .getCode() + .getCode() + .equals("priority")) { + priorityToCheck = ((UsageContext) originalValue) + .getValueCodeableConcept() + .getCodingFirstRep() + .getCode(); + } + if (priorityToCheck != null) { + this.priority.operation = operation; + } + } else { + this.operations.add(operation); + } + } + } + + private void updateCodeOperation(String codeToCheck, Operation operation) { + if (codeToCheck != null) { + final String codeNotNull = codeToCheck; + this.codes.stream() + .filter(code -> code.code != null) + .filter(code -> code.code.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 { + public RelatedArtifact fullRelatedArtifact; + public List conditions = new ArrayList<>(); + public codeableConceptWithOperation priority = new codeableConceptWithOperation(null); + + public static class codeableConceptWithOperation { + public CodeableConcept value; + public Operation operation; + + codeableConceptWithOperation(CodeableConcept e) { + this.value = e; + } + } + + RelatedArtifactUrlWithOperation(RelatedArtifact relatedArtifact) { + if (relatedArtifact != null) { + this.value = relatedArtifact.getResource(); + this.conditions = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmCondition).stream() + .map(e -> new codeableConceptWithOperation((CodeableConcept) e.getValue())) + .collect(Collectors.toList()); + var priorities = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmPriority).stream() + .map(e -> (CodeableConcept) e.getValue()) + .collect(Collectors.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 { + public ValueAndOperation purpose = new ValueAndOperation(); + public ValueAndOperation effectiveStart = new ValueAndOperation(); + public ValueAndOperation releaseDate = new ValueAndOperation(); + public List relatedArtifacts = new ArrayList<>(); + + 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))); + } + } + + private Optional getRelatedArtifactFromUrl(String target) { + return this.relatedArtifacts.stream() + .filter(ra -> ra.value != null && ra.value.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)) { + if (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, ChangeLog parent) { + if (type != null) { + super.addOperation(type, path, currentValue, originalValue, parent); + var newOperation = new Operation(type, path, currentValue, originalValue); + Optional operationTarget = Optional.ofNullable(null); + if (path != null && path.contains("elatedArtifact")) { + if (currentValue instanceof RelatedArtifact) { + operationTarget = getRelatedArtifactFromUrl(((RelatedArtifact) currentValue).getResource()); + } else if (originalValue instanceof RelatedArtifact) { + operationTarget = + getRelatedArtifactFromUrl(((RelatedArtifact) originalValue).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) { + tryAddConditionOperation( + (Extension) currentValue, operationTarget.orElse(null), newOperation); + tryAddPriorityOperation( + (Extension) currentValue, operationTarget.orElse(null), newOperation); + } else if (originalValue instanceof Extension) { + tryAddConditionOperation( + (Extension) originalValue, operationTarget.orElse(null), newOperation); + tryAddPriorityOperation( + (Extension) originalValue, operationTarget.orElse(null), newOperation); + } else { + operationTarget.get().operation = newOperation; + } + } + } else if (path.equals("name")) { + this.name.setOperation(newOperation); + } else if (path.contains("purpose")) { + this.purpose.setOperation(newOperation); + } else if (path.equals("approvalDate")) { + this.releaseDate.setOperation(newOperation); + } else if (path.contains("effectivePeriod")) { + this.effectiveStart.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..c4c8c4e311 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/ICreateChangelogProcessor.java @@ -0,0 +1,9 @@ +package org.opencds.cqf.fhir.cr.common; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Endpoint; + +public interface ICreateChangelogProcessor extends IOperationProcessor { + + IBaseResource createChangelog(IBaseResource source, IBaseResource target, Endpoint terminologyEndpoint); +} 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..6cb00dfb4a 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,11 @@ public , R extends IBaseResource> IBaseParamete null, terminologyEndpoint); } + + public , R extends IBaseResource> IBaseResource createChangelog( + Either3 sourceLibrary, Either3 targetLibrary, Endpoint terminologyEndpoint) { + var processor = createChangelogProcessor != null ? createChangelogProcessor : new CreateChangelogProcessor(); + return processor.createChangelog( + resolveLibrary(sourceLibrary), resolveLibrary(targetLibrary), terminologyEndpoint); + } } From c59a5bf23b02f412b1b4abda69af30a7943ad566 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 5 Feb 2026 09:57:28 -0800 Subject: [PATCH 02/22] Refactor - clean up sonar issues --- .../common/HapiCreateChangelogProcessor.java | 2 +- .../cr/common/CreateChangelogProcessor.java | 907 ++++++++++-------- .../cqf/fhir/cr/crmi/TransformProperties.java | 1 + 3 files changed, 521 insertions(+), 389 deletions(-) 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 index 228d1ed077..6bd64fa978 100644 --- 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 @@ -201,7 +201,7 @@ private void processChanges( // 4) Add a new operation to the ChangeLog page.addOperation( - type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null), changelog); + type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null)); } } } 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 index 8c53e65018..22ac23ba51 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -18,12 +19,16 @@ 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.jetbrains.annotations.Nullable; 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; @@ -46,18 +51,33 @@ public IBaseResource createChangelog(IBaseResource source, IBaseResource target, } public static class ChangeLog { - public List> pages; - public String manifestUrl; + 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.pages = new ArrayList<>(); this.manifestUrl = url; } - public Page addPage(String url, T oldData, T newData) { - var page = new Page(url, oldData, newData); - this.pages.add(page); - return page; + 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) @@ -65,13 +85,13 @@ public Page addPage(ValueSet sourceResource, ValueSet targetResou if (sourceResource != null && targetResource != null && !sourceResource.getUrl().equals(targetResource.getUrl())) { - throw new UnprocessableEntityException("URLs don't match"); + throw new UnprocessableEntityException(URLS_DONT_MATCH); } // Map< [Code], [Object with code, version, system, etc.] > - Map codeMap = new HashMap(); + Map codeMap = new HashMap<>(); // Map< [URL], Map <[Version], [Object with name, version, and other metadata] >> Map> leafMetadataMap = - new HashMap>(); + new HashMap<>(); updateCodeMapAndLeafMetadataMap(codeMap, leafMetadataMap, sourceResource, cache); updateCodeMapAndLeafMetadataMap(codeMap, leafMetadataMap, targetResource, cache); var oldData = sourceResource == null @@ -100,16 +120,23 @@ public Page addPage(ValueSet sourceResource, ValueSet targetResou codeMap, leafMetadataMap, getPriority(targetResource).orElse(null)); - var url = sourceResource == null ? targetResource.getUrl() : sourceResource.getUrl(); - var page = new Page(url, oldData, newData); + 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("priority")) + && uc.getCode().getCode().equals(TransformProperties.vsmPriorityCode)) .findAny() .map(uc -> uc.getValueCodeableConcept().getCodingFirstRep().getCode()); } @@ -122,59 +149,66 @@ private void updateCodeMapAndLeafMetadataMap( if (valueSet != null) { var leafData = updateLeafMap(leafMap, valueSet); if (valueSet.getCompose().hasInclude()) { - valueSet.getCompose().getInclude().forEach(concept -> { - if (concept.hasConcept()) { - var codeSystemName = ValueSetChild.Code.getCodeSystemName(concept.getSystem()); - var codeSystemOid = ValueSetChild.Code.getCodeSystemOid(concept.getSystem()); - 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)); - } - 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); - }); - } - }); + handleValueSetInclude(codeMap, leafMap, valueSet, cache, leafData); } if (valueSet.getExpansion().hasContains()) { - valueSet.getExpansion().getContains().forEach((cnt) -> { - if (!codeMap.containsKey(cnt.getCode())) { - var codeSystemName = ValueSetChild.Code.getCodeSystemName(cnt.getSystem()); - var codeSystemOid = ValueSetChild.Code.getCodeSystemOid(cnt.getSystem()); - 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)); - } - mapExpansionContainsToCodeMap( - codeMap, - cnt, - Canonicals.getIdPart(valueSet.getUrl()), - valueSet.getName(), - valueSet.getTitle(), - valueSet.getUrl()); - } - }); + 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)); } } @@ -186,9 +220,9 @@ private ValueSetChild.Leaf updateLeafMap( } var versionedLeafMap = leafMap.get(valueSet.getUrl()); - ; + if (!leafMap.containsKey(valueSet.getUrl())) { - versionedLeafMap = new HashMap(); + versionedLeafMap = new HashMap<>(); leafMap.put(valueSet.getUrl(), versionedLeafMap); } @@ -256,46 +290,35 @@ public Page addPage(Library sourceResource, Library targetResource if (sourceResource != null && targetResource != null && !sourceResource.getUrl().equals(targetResource.getUrl())) { - throw new UnprocessableEntityException("URLs don't match"); + throw new UnprocessableEntityException(URLS_DONT_MATCH); } - var oldData = sourceResource == null - ? null - : new LibraryChild( - sourceResource.getName(), - sourceResource.getPurpose(), - sourceResource.getTitle(), - sourceResource.getIdPart(), - sourceResource.getVersion(), - sourceResource.getUrl(), - Optional.ofNullable((Period) sourceResource.getEffectivePeriod()) - .map(p -> p.getStart()) - .map(s -> s.toString()) - .orElse(null), - Optional.ofNullable(sourceResource.getApprovalDate()) - .map(s -> s.toString()) - .orElse(null), - sourceResource.getRelatedArtifact()); - var newData = targetResource == null + 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; + } + + @Nullable + private static LibraryChild getLibraryChild(Library library) { + return library == null ? null : new LibraryChild( - targetResource.getName(), - targetResource.getPurpose(), - targetResource.getTitle(), - targetResource.getIdPart(), - targetResource.getVersion(), - targetResource.getUrl(), - Optional.ofNullable((Period) targetResource.getEffectivePeriod()) - .map(p -> p.getStart()) - .map(s -> s.toString()) + 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(targetResource.getApprovalDate()) - .map(s -> s.toString()) + Optional.ofNullable(library.getApprovalDate()) + .map(Date::toString) .orElse(null), - targetResource.getRelatedArtifact()); - var url = sourceResource == null ? targetResource.getUrl() : sourceResource.getUrl(); - var page = new Page(url, oldData, newData); - this.pages.add(page); - return page; + library.getRelatedArtifact()); } public Page addPage(PlanDefinition sourceResource, PlanDefinition targetResource) @@ -303,30 +326,28 @@ public Page addPage(PlanDefinition sourceResource, PlanDefi if (sourceResource != null && targetResource != null && !sourceResource.getUrl().equals(targetResource.getUrl())) { - throw new UnprocessableEntityException("URLs don't match"); + throw new UnprocessableEntityException(URLS_DONT_MATCH); } - var oldData = sourceResource == null - ? null - : new PlanDefinitionChild( - sourceResource.getTitle(), - sourceResource.getIdPart(), - sourceResource.getVersion(), - sourceResource.getName(), - sourceResource.getUrl()); - var newData = targetResource == null - ? null - : new PlanDefinitionChild( - targetResource.getTitle(), - targetResource.getIdPart(), - targetResource.getVersion(), - targetResource.getName(), - targetResource.getUrl()); - var url = sourceResource == null ? targetResource.getUrl() : sourceResource.getUrl(); - var page = new Page(url, oldData, newData); + 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; } + @Nullable + 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 @@ -347,7 +368,7 @@ public Page addPage(IBaseResource sourceResource, IBaseResource targ null, url, targetResource.fhirType()); - var page = new Page(url, oldData, newData); + var page = new Page<>(url, oldData, newData); this.pages.add(page); return page; } @@ -367,36 +388,33 @@ public void handleRelatedArtifacts() { if (manifestNewData != null) { for (final var page : this.pages) { if (page.oldData instanceof ValueSetChild) { - for (final var ra : manifestOldData.relatedArtifacts) { - ((ValueSetChild) page.oldData) - .leafValuesets.stream() - .filter(leafValueSet -> leafValueSet.memberOid != null - && leafValueSet.memberOid.equals( - Canonicals.getIdPart(ra.value))) - .forEach(leafValueSet -> { - updateConditions(ra, leafValueSet); - updatePriorities(ra, leafValueSet); - }); - } + updateConditionsAndPriorities(manifestOldData, + (ValueSetChild) page.oldData); } if (page.newData instanceof ValueSetChild) { - for (final var ra : manifestNewData.relatedArtifacts) { - ((ValueSetChild) page.newData) - .leafValuesets.stream() - .filter(leafValueSet -> leafValueSet.memberOid != null - && leafValueSet.memberOid.equals( - Canonicals.getIdPart(ra.value))) - .forEach(leafValueSet -> { - updateConditions(ra, leafValueSet); - updatePriorities(ra, leafValueSet); - }); - } + updateConditionsAndPriorities(manifestNewData, + (ValueSetChild) page.newData); } } } } } + 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) { @@ -415,38 +433,55 @@ private void updatePriorities(RelatedArtifactUrlWithOperation ra, ChangeLog.Valu } public static class Page { - public T oldData; - public T newData; - public String url; - public String resourceType; + 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.resourceType != null) { - this.resourceType = oldData.resourceType; - } else if (newData != null && newData.resourceType != null) { - this.resourceType = newData.resourceType; + 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, ChangeLog parent) { + String type, String path, Object currentValue, Object originalValue) { if (type != null) { switch (type) { - case "replace": - addReplaceOperation(type, path, currentValue, originalValue, parent); - break; - case "delete": - addDeleteOperation(type, path, null, originalValue, parent); - break; - case "insert": - addInsertOperation(type, path, currentValue, null, parent); - break; - default: - throw new UnprocessableEntityException( - "Unknown type provided when adding an operation to the ChangeLog"); + 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( @@ -455,40 +490,52 @@ public void addOperation( } void addInsertOperation( - String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { - if (type != "insert") { - throw new UnprocessableEntityException("wrong type"); + String type, String path, Object currentValue) { + if (!type.equals(INSERT)) { + throw new UnprocessableEntityException(WRONG_TYPE); } - this.newData.addOperation(type, path, currentValue, originalValue, parent); + this.newData.addOperation(type, path, currentValue, null); } void addDeleteOperation( - String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { - if (type != "delete") { - throw new UnprocessableEntityException("wrong type"); + String type, String path, Object originalValue) { + if (!type.equals(DELETE)) { + throw new UnprocessableEntityException(WRONG_TYPE); } - this.oldData.addOperation(type, path, currentValue, originalValue, parent); + this.oldData.addOperation(type, path, null, originalValue); } void addReplaceOperation( - String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { - if (type != "replace") { - throw new UnprocessableEntityException("wrong type"); + String type, String path, Object currentValue, Object originalValue) { + if (!type.equals(REPLACE)) { + throw new UnprocessableEntityException(WRONG_TYPE); } - this.oldData.addOperation(type, path, currentValue, null, parent); - this.newData.addOperation(type, path, null, originalValue, parent); + this.oldData.addOperation(type, path, currentValue, null); + this.newData.addOperation(type, path, null, originalValue); } } public static class ValueAndOperation { - public String value; - public Operation operation; + 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 == operation.type - && this.operation.path == operation.path + && 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"); } @@ -498,45 +545,87 @@ public void setOperation(Operation operation) { } public static class Operation { - public String type; - public String path; - public Object newValue; - public Object oldValue; - - Operation(String type, String path, IBase newValue, IBase original) { - this.type = type; - this.path = path; - this.oldValue = original; - this.newValue = newValue; - } + 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) { - this.oldValue = ((IPrimitiveType) originalValue).getValue(); + 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) { - this.newValue = ((IPrimitiveType) newValue).getValue(); + 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 { - public ValueAndOperation title = new ValueAndOperation(); - public ValueAndOperation id = new ValueAndOperation(); - public ValueAndOperation version = new ValueAndOperation(); - public ValueAndOperation name = new ValueAndOperation(); - public ValueAndOperation url = new ValueAndOperation(); - public String resourceType; + 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)) { @@ -558,7 +647,7 @@ public static class PageBase { } public void addOperation( - String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { + String type, String path, Object currentValue, Object originalValue) { if (type != null) { var newOp = new Operation(type, path, currentValue, originalValue); if (path.equals("id")) { @@ -577,10 +666,26 @@ public void addOperation( } public static class ValueSetChild extends PageBase { - public List codes = new ArrayList<>(); - public List leafValuesets = new ArrayList<>(); - public List operations = new ArrayList<>(); - public ValueAndOperation priority = new ValueAndOperation(); + 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 { public String id; @@ -672,8 +777,8 @@ public Operation getOperation() { public void setOperation(Operation operation) { if (operation != null) { if (this.operation != null - && this.operation.type == operation.type - && this.operation.path == operation.path + && 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"); } @@ -721,9 +826,9 @@ 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(c -> c.copy()).collect(Collectors.toList()); + this.codeSystems.stream().map(NameAndOid::copy).collect(Collectors.toList()); copy.conditions = - this.conditions.stream().map(c -> c.copy()).collect(Collectors.toList()); + 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; @@ -782,10 +887,10 @@ public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { } if (compose != null) { compose.stream() - .filter(cmp -> cmp.hasValueSet()) + .filter(ConceptSetComponent::hasValueSet) .flatMap(c -> c.getValueSet().stream()) - .filter(vs -> vs.hasValue()) - .map(vs -> vs.getValue()) + .filter(PrimitiveType::hasValue) + .map(PrimitiveType::getValue) .forEach(vs -> { // sometimes the value set reference is unversioned - implying that the latest version // should be used @@ -801,12 +906,12 @@ public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { .next() .getValue(); // creating a new object because modifying it causes weirdness later - leafValuesets.add(latest.copy()); + 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()); + leafValueSets.add(leaf.copy()); } }); } @@ -817,123 +922,147 @@ public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { @Override public void addOperation( - String type, String path, Object newValue, Object originalValue, ChangeLog parent) { + String type, String path, Object newValue, Object originalValue) { if (type != null) { - super.addOperation(type, path, newValue, originalValue, parent); + super.addOperation(type, path, newValue, originalValue); var operation = new Operation(type, path, newValue, originalValue); if (path.contains("compose")) { - // 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 - && ((ValueSet.ValueSetComposeComponent) newValue) - .getIncludeFirstRep() - .hasValueSet()) { - urlsToCheck = ((ValueSet.ValueSetComposeComponent) newValue) - .getInclude().stream() - .filter(include -> include.hasValueSet()) - .flatMap(include -> include.getValueSet().stream()) - .filter(canonical -> canonical.hasValue()) - .map(canonical -> canonical.getValue()) - .collect(Collectors.toList()); - updatedOperations = urlsToCheck.stream() - .map(url -> new Operation( - type, path, url, type.equals("replace") ? originalValue : null)) - .collect(Collectors.toList()); - } else if (originalValue instanceof ValueSet.ValueSetComposeComponent - && ((ValueSet.ValueSetComposeComponent) originalValue) - .getIncludeFirstRep() - .hasValueSet()) { - urlsToCheck = ((ValueSet.ValueSetComposeComponent) originalValue) - .getInclude().stream() - .filter(include -> include.hasValueSet()) - .flatMap(include -> include.getValueSet().stream()) - .filter(canonical -> canonical.hasValue()) - .map(canonical -> canonical.getValue()) - .collect(Collectors.toList()); - updatedOperations = urlsToCheck.stream() - .map(url -> - new Operation(type, path, type.equals("replace") ? newValue : null, url)) - .collect(Collectors.toList()); - } - 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); - } - } - } - } + addOperationHandleCompose(type, path, newValue, originalValue, operation); } else if (path.contains("expansion")) { - if (path.contains("expansion.contains[")) { - // if the codes themselves changed - 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) { - codeToCheck = ((ValueSet.ValueSetExpansionContainsComponent) originalValue).getCode(); - } - updateCodeOperation(codeToCheck, operation); - } else if (newValue instanceof ValueSet.ValueSetExpansionComponent - || originalValue instanceof ValueSet.ValueSetExpansionComponent) { - var contains = newValue instanceof ValueSet.ValueSetExpansionComponent - ? (ValueSet.ValueSetExpansionComponent) newValue - : (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); - }); - } + addOperationHandleExpansion(type, path, newValue, originalValue, operation); } else if (path.contains("useContext")) { - String priorityToCheck = null; - if (newValue instanceof UsageContext - && ((UsageContext) newValue) - .getCode() - .getSystem() - .equals(TransformProperties.usPHUsageContextType) - && ((UsageContext) newValue).getCode().getCode().equals("priority")) { - priorityToCheck = ((UsageContext) newValue) - .getValueCodeableConcept() - .getCodingFirstRep() - .getCode(); - } else if (originalValue instanceof UsageContext - && ((UsageContext) originalValue) - .getCode() - .getSystem() - .equals(TransformProperties.usPHUsageContextType) - && ((UsageContext) originalValue) - .getCode() - .getCode() - .equals("priority")) { - priorityToCheck = ((UsageContext) originalValue) - .getValueCodeableConcept() - .getCodingFirstRep() - .getCode(); - } - if (priorityToCheck != null) { - this.priority.operation = operation; - } + addOperationHandleUseContext(newValue, originalValue, operation); } else { this.operations.add(operation); } } } + 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); + }); + } + } + + @Nullable + 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) { + codeToCheck = ((ValueSet.ValueSetExpansionContainsComponent) originalValue).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.vsmPriorityCode)) { + priorityToCheck = newUseContext + .getValueCodeableConcept() + .getCodingFirstRep() + .getCode(); + } else if (originalValue instanceof UsageContext originalUseContext + && originalUseContext + .getCode() + .getSystem() + .equals(TransformProperties.usPHUsageContextType) + && originalUseContext + .getCode() + .getCode() + .equals(TransformProperties.vsmPriorityCode)) { + 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; @@ -942,13 +1071,13 @@ private void updateCodeOperation(String codeToCheck, Operation operation) { .filter(code -> code.code.equals(codeNotNull)) .findAny() .ifPresentOrElse( - code -> { - code.setOperation(operation); - }, - () -> { + code -> + code.setOperation(operation) + , + () -> // drop unmatched operations in the base operations list - this.operations.add(operation); - }); + this.operations.add(operation) + ); } } } @@ -981,13 +1110,13 @@ public static class codeableConceptWithOperation { RelatedArtifactUrlWithOperation(RelatedArtifact relatedArtifact) { if (relatedArtifact != null) { - this.value = relatedArtifact.getResource(); + this.setValue(relatedArtifact.getResource()); this.conditions = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmCondition).stream() .map(e -> new codeableConceptWithOperation((CodeableConcept) e.getValue())) - .collect(Collectors.toList()); + .toList(); var priorities = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmPriority).stream() .map(e -> (CodeableConcept) e.getValue()) - .collect(Collectors.toList()); + .toList(); if (priorities.size() > 1) { throw new UnprocessableEntityException("too many priorities"); } else if (priorities.size() == 1) { @@ -1034,7 +1163,7 @@ public static class LibraryChild extends PageBase { private Optional getRelatedArtifactFromUrl(String target) { return this.relatedArtifacts.stream() - .filter(ra -> ra.value != null && ra.value.equals(target)) + .filter(ra -> ra.getValue() != null && ra.getValue().equals(target)) .findAny(); } @@ -1055,16 +1184,13 @@ private void tryAddConditionOperation( .getCodingFirstRep() .getCode())) .findAny() - .ifPresent(condition -> { - condition.operation = newOperation; - }); + .ifPresent(condition -> condition.operation = newOperation); } } private void tryAddPriorityOperation( Extension maybePriority, RelatedArtifactUrlWithOperation target, Operation newOperation) { - if (maybePriority.getUrl().equals(TransformProperties.vsmPriority)) { - if (target.priority.value != null + if (maybePriority.getUrl().equals(TransformProperties.vsmPriority) && (target.priority.value != null && target.priority .value .getCodingFirstRep() @@ -1078,78 +1204,83 @@ private void tryAddPriorityOperation( .getCode() .equals(((CodeableConcept) maybePriority.getValue()) .getCodingFirstRep() - .getCode())) { + .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"; + newOperation.type = REPLACE; target.priority.operation = newOperation; - } - ; + } } @Override public void addOperation( - String type, String path, Object currentValue, Object originalValue, ChangeLog parent) { + String type, String path, Object currentValue, Object originalValue) { if (type != null) { - super.addOperation(type, path, currentValue, originalValue, parent); + super.addOperation(type, path, currentValue, originalValue); var newOperation = new Operation(type, path, currentValue, originalValue); - Optional operationTarget = Optional.ofNullable(null); if (path != null && path.contains("elatedArtifact")) { - if (currentValue instanceof RelatedArtifact) { - operationTarget = getRelatedArtifactFromUrl(((RelatedArtifact) currentValue).getResource()); - } else if (originalValue instanceof RelatedArtifact) { - operationTarget = - getRelatedArtifactFromUrl(((RelatedArtifact) originalValue).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) { - tryAddConditionOperation( - (Extension) currentValue, operationTarget.orElse(null), newOperation); - tryAddPriorityOperation( - (Extension) currentValue, operationTarget.orElse(null), newOperation); - } else if (originalValue instanceof Extension) { - tryAddConditionOperation( - (Extension) originalValue, operationTarget.orElse(null), newOperation); - tryAddPriorityOperation( - (Extension) originalValue, operationTarget.orElse(null), newOperation); - } else { - operationTarget.get().operation = newOperation; - } - } - } else if (path.equals("name")) { - this.name.setOperation(newOperation); - } else if (path.contains("purpose")) { + 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.equals("approvalDate")) { + } else if (path != null && path.equals("approvalDate")) { this.releaseDate.setOperation(newOperation); - } else if (path.contains("effectivePeriod")) { + } 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/crmi/TransformProperties.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/TransformProperties.java index 42478d6f36..998a0e70f6 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 vsmPriorityCode = "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 = From 96337323d2af6ba8424eb2c89fc1a919bb5712b0 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 19 Feb 2026 10:58:09 -0800 Subject: [PATCH 03/22] Cleanup --- .../common/HapiCreateChangelogProcessor.java | 10 +- .../cr/common/CreateChangelogProcessor.java | 236 +++++++----------- .../cr/common/ICreateChangelogProcessor.java | 3 +- .../cqf/fhir/cr/library/LibraryProcessor.java | 4 +- 4 files changed, 106 insertions(+), 147 deletions(-) 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 index 6bd64fa978..3cfd909425 100644 --- 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 @@ -33,12 +33,12 @@ 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.Endpoint; 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.CreateChangelogProcessor.ChangeLog; @@ -66,7 +66,8 @@ public HapiCreateChangelogProcessor(IRepository repository) { } @Override - public IBaseResource createChangelog(IBaseResource source, IBaseResource target, Endpoint terminologyEndpoint) { + public IBaseResource createChangelog( + IBaseResource source, IBaseResource target, IBaseResource terminologyEndpoint) { // 1) Use package to get a pair of bundles ExecutorService service = Executors.newCachedThreadPool(); @@ -74,7 +75,7 @@ public IBaseResource createChangelog(IBaseResource source, IBaseResource target, Bundle sourceBundle; Bundle targetBundle; Parameters params = new Parameters(); - params.addParameter().setName("terminologyEndpoint").setResource(terminologyEndpoint); + params.addParameter().setName("terminologyEndpoint").setResource((Resource) terminologyEndpoint); try { packages = service.invokeAll(Arrays.asList( () -> packageProcessor.packageResource(source, params), @@ -200,8 +201,7 @@ private void processChanges( } // 4) Add a new operation to the ChangeLog - page.addOperation( - type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null)); + page.addOperation(type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null)); } } } 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 index 22ac23ba51..d8a09069c2 100644 --- 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 @@ -15,7 +15,6 @@ 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.Endpoint; import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Library; @@ -45,7 +44,8 @@ public CreateChangelogProcessor() { } @Override - public IBaseResource createChangelog(IBaseResource source, IBaseResource target, Endpoint terminologyEndpoint) { + public IBaseResource createChangelog( + IBaseResource source, IBaseResource target, IBaseResource terminologyEndpoint) { logger.info("Unable to perform $create-changelog outside of HAPI context"); return new Parameters(); } @@ -90,8 +90,7 @@ public Page addPage(ValueSet sourceResource, ValueSet targetResou // 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<>(); + Map> leafMetadataMap = new HashMap<>(); updateCodeMapAndLeafMetadataMap(codeMap, leafMetadataMap, sourceResource, cache); updateCodeMapAndLeafMetadataMap(codeMap, leafMetadataMap, targetResource, cache); var oldData = sourceResource == null @@ -157,14 +156,17 @@ private void updateCodeMapAndLeafMetadataMap( } } - private void handleValueSetInclude(Map codeMap, - Map> leafMap, ValueSet valueSet, - DiffCache cache, ValueSetChild.Leaf 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, + codeMap, concept, Canonicals.getIdPart(valueSet.getUrl()), valueSet.getName(), @@ -184,18 +186,17 @@ private void handleValueSetInclude(Map codeMap, }); } - private void handleValueSetContains(Map codeMap, ValueSet valueSet, - ValueSetChild.Leaf leafData) { + 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()); + codeMap, + cnt, + Canonicals.getIdPart(valueSet.getUrl()), + valueSet.getName(), + valueSet.getTitle(), + valueSet.getUrl()); } }); } @@ -204,11 +205,9 @@ 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)); + .anyMatch(nameAndOid -> nameAndOid.oid != null && nameAndOid.oid.equals(codeSystemOid)); if (!doesOidExistInList) { - leafData.codeSystems.add( - new ValueSetChild.Leaf.NameAndOid(codeSystemName, codeSystemOid)); + leafData.codeSystems.add(new ValueSetChild.Leaf.NameAndOid(codeSystemName, codeSystemOid)); } } @@ -388,30 +387,25 @@ public void handleRelatedArtifacts() { if (manifestNewData != null) { for (final var page : this.pages) { if (page.oldData instanceof ValueSetChild) { - updateConditionsAndPriorities(manifestOldData, - (ValueSetChild) page.oldData); + updateConditionsAndPriorities(manifestOldData, (ValueSetChild) page.oldData); } if (page.newData instanceof ValueSetChild) { - updateConditionsAndPriorities(manifestNewData, - (ValueSetChild) page.newData); + updateConditionsAndPriorities(manifestNewData, (ValueSetChild) page.newData); } } } } } - private void updateConditionsAndPriorities(LibraryChild manifestData, - ValueSetChild pageData) { + 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); - }); + pageData.leafValueSets.stream() + .filter(leafValueSet -> leafValueSet.memberOid != null + && leafValueSet.memberOid.equals(Canonicals.getIdPart(ra.getValue()))) + .forEach(leafValueSet -> { + updateConditions(ra, leafValueSet); + updatePriorities(ra, leafValueSet); + }); } } @@ -473,8 +467,7 @@ public void setResourceType(String resourceType) { } } - public void addOperation( - String type, String path, Object currentValue, Object originalValue) { + public void addOperation(String type, String path, Object currentValue, Object originalValue) { if (type != null) { switch (type) { case REPLACE -> addReplaceOperation(type, path, currentValue, originalValue); @@ -489,24 +482,21 @@ public void addOperation( } } - void addInsertOperation( - String type, String path, Object currentValue) { + 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) { + 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) { + void addReplaceOperation(String type, String path, Object currentValue, Object originalValue) { if (!type.equals(REPLACE)) { throw new UnprocessableEntityException(WRONG_TYPE); } @@ -646,8 +636,7 @@ public String getResourceType() { this.resourceType = resourceType; } - public void addOperation( - String type, String path, Object currentValue, Object originalValue) { + 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")) { @@ -827,8 +816,7 @@ public Leaf copy() { 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.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; @@ -921,8 +909,7 @@ public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { } @Override - public void addOperation( - String type, String path, Object newValue, Object originalValue) { + 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); @@ -938,8 +925,8 @@ public void addOperation( } } - private void addOperationHandleCompose(String type, String path, Object newValue, Object originalValue, - Operation operation) { + 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 @@ -950,34 +937,26 @@ private void addOperationHandleCompose(String type, String path, Object newValue && ((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(); + && 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)) + .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(); + && 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)) + .map(url -> new Operation(type, path, type.equals(REPLACE) ? newValue : null, url)) .toList(); } handleUrlsToCheck(urlsToCheck, updatedOperations); @@ -996,8 +975,8 @@ private void handleUrlsToCheck(List urlsToCheck, List updated } } - private void addOperationHandleExpansion(String type, String path, Object newValue, Object originalValue, - Operation operation) { + 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); @@ -1035,24 +1014,15 @@ private static String getCodeToCheck(Object newValue, Object originalValue) { 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().getSystem().equals(TransformProperties.usPHUsageContextType) && newUseContext.getCode().getCode().equals(TransformProperties.vsmPriorityCode)) { priorityToCheck = newUseContext .getValueCodeableConcept() .getCodingFirstRep() .getCode(); } else if (originalValue instanceof UsageContext originalUseContext - && originalUseContext - .getCode() - .getSystem() - .equals(TransformProperties.usPHUsageContextType) - && originalUseContext - .getCode() - .getCode() - .equals(TransformProperties.vsmPriorityCode)) { + && originalUseContext.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) + && originalUseContext.getCode().getCode().equals(TransformProperties.vsmPriorityCode)) { priorityToCheck = originalUseContext .getValueCodeableConcept() .getCodingFirstRep() @@ -1071,13 +1041,10 @@ private void updateCodeOperation(String codeToCheck, Operation operation) { .filter(code -> code.code.equals(codeNotNull)) .findAny() .ifPresentOrElse( - code -> - code.setOperation(operation) - , + code -> code.setOperation(operation), () -> - // drop unmatched operations in the base operations list - this.operations.add(operation) - ); + // drop unmatched operations in the base operations list + this.operations.add(operation)); } } } @@ -1190,35 +1157,34 @@ private void tryAddConditionOperation( 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; - + 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) { + 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); @@ -1236,16 +1202,15 @@ public void addOperation( } } - private void addOperationHandleRelatedArtifacts(String path, Object currentValue, Object originalValue, Operation 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()); + operationTarget = getRelatedArtifactFromUrl(originalRelatedArtifact.getResource()); } else if (path.contains("[")) { - var matcher = Pattern.compile("relatedArtifact\\[(\\d+)]") - .matcher(path); + 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)); @@ -1253,29 +1218,22 @@ private void addOperationHandleRelatedArtifacts(String path, Object currentValue } if (operationTarget.isPresent()) { if (path.contains("xtension[")) { - var matcher = - Pattern.compile("xtension\\[(\\d+)]").matcher(path); + 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); + 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); + 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); + 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 index c4c8c4e311..62b343b473 100644 --- 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 @@ -1,9 +1,8 @@ package org.opencds.cqf.fhir.cr.common; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Endpoint; public interface ICreateChangelogProcessor extends IOperationProcessor { - IBaseResource createChangelog(IBaseResource source, IBaseResource target, Endpoint terminologyEndpoint); + IBaseResource createChangelog(IBaseResource source, IBaseResource target, IBaseResource terminologyEndpoint); } 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 6cb00dfb4a..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 @@ -293,7 +293,9 @@ public , R extends IBaseResource> IBaseParamete } public , R extends IBaseResource> IBaseResource createChangelog( - Either3 sourceLibrary, Either3 targetLibrary, Endpoint terminologyEndpoint) { + Either3 sourceLibrary, + Either3 targetLibrary, + IBaseResource terminologyEndpoint) { var processor = createChangelogProcessor != null ? createChangelogProcessor : new CreateChangelogProcessor(); return processor.createChangelog( resolveLibrary(sourceLibrary), resolveLibrary(targetLibrary), terminologyEndpoint); From a086425987f50bbcb93c66963661cf0ea508be8a Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 19 Feb 2026 11:28:24 -0800 Subject: [PATCH 04/22] Remove annotation --- .../opencds/cqf/fhir/cr/common/CreateChangelogProcessor.java | 4 ---- 1 file changed, 4 deletions(-) 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 index d8a09069c2..0264a2df7b 100644 --- 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 @@ -27,7 +27,6 @@ import org.hl7.fhir.r4.model.UsageContext; import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; -import org.jetbrains.annotations.Nullable; 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; @@ -299,7 +298,6 @@ public Page addPage(Library sourceResource, Library targetResource return page; } - @Nullable private static LibraryChild getLibraryChild(Library library) { return library == null ? null @@ -335,7 +333,6 @@ public Page addPage(PlanDefinition sourceResource, PlanDefi return page; } - @Nullable private static PlanDefinitionChild getPlanDefinitionChild(PlanDefinition resource) { return resource == null ? null @@ -998,7 +995,6 @@ private void addOperationHandleExpansion( } } - @Nullable private static String getCodeToCheck(Object newValue, Object originalValue) { String codeToCheck = null; if (newValue instanceof IPrimitiveType || originalValue instanceof IPrimitiveType) { From 9b594a8370cfc14e13143900166e1e9ae140dbc1 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 11:07:53 -0800 Subject: [PATCH 05/22] Sonar fixes --- .../common/HapiCreateChangelogProcessor.java | 172 ++++++++++-------- .../fhir/cr/common/ArtifactDiffProcessor.java | 12 +- 2 files changed, 102 insertions(+), 82 deletions(-) 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 index 3cfd909425..f713b5f404 100644 --- 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 @@ -52,14 +52,12 @@ @SuppressWarnings("UnstableApiUsage") public class HapiCreateChangelogProcessor implements ICreateChangelogProcessor { - private final IRepository repository; private final FhirVersionEnum fhirVersion; private final PackageProcessor packageProcessor; private final HapiArtifactDiffProcessor hapiArtifactDiffProcessor; public HapiCreateChangelogProcessor(IRepository repository) { - this.repository = repository; this.fhirVersion = repository.fhirContext().getVersion().getVersion(); this.packageProcessor = new PackageProcessor(repository); this.hapiArtifactDiffProcessor = new HapiArtifactDiffProcessor(repository); @@ -85,18 +83,52 @@ public IBaseResource createChangelog( service.shutdownNow(); } catch (InterruptedException | ExecutionException e) { service.shutdownNow(); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } throw new UnprocessableEntityException(e.getMessage()); } // 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(); - Optional sourceResource = Optional.empty(); - Optional targetResource = Optional.empty(); 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())) { - sourceResource = Optional.of((Library) metadataResource); + cache.addSource(metadataResource.getUrl(), metadataResource); } } } @@ -104,33 +136,11 @@ public IBaseResource createChangelog( if (entry.hasResource() && entry.getResource() instanceof MetadataResource metadataResource) { cache.addTarget(metadataResource.getUrl() + "|" + metadataResource.getVersion(), metadataResource); if (metadataResource.getIdPart().equals(target.getIdElement().getIdPart())) { - targetResource = Optional.of((Library) metadataResource); + cache.addTarget(metadataResource.getUrl(), metadataResource); } } } - - // 3) Use cached resources to create diff and changelog - var targetAdapter = IAdapterFactory.forFhirVersion(FhirVersionEnum.R4) - .createKnowledgeArtifactAdapter(targetResource.orElse(null)); - var diffParameters = hapiArtifactDiffProcessor.getArtifactDiff( - sourceResource.orElse(null), targetResource.orElse(null), 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 cache; } private ObjectMapper createSerializer() { @@ -146,67 +156,69 @@ private ObjectMapper createSerializer() { 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 resources = cache.getResourcesForUrl(url); var resourceType = Canonicals.getResourceType(url); // Check if the resource pair was already processed var wasPageAlreadyProcessed = changelog.getPage(url).isPresent(); - if (!resources.isEmpty() && !wasPageAlreadyProcessed) { - final MetadataResource sourceResource = resources.get(0).isSource - ? resources.get(0).resource - : (resources.size() > 1 ? resources.get(1).resource : null); - final MetadataResource targetResource = resources.get(0).isSource - ? (resources.size() > 1 ? resources.get(1).resource : null) - : resources.get(0).resource; - // don't generate changeLog pages for non-grouper ValueSets - if (resourceType.equals("ValueSet") + if (!wasPageAlreadyProcessed && cache.getSourceResourceForUrl(url).isPresent() && cache.getTargetResourceForUrl(url).isPresent()) { + final MetadataResource sourceResource = cache.getSourceResourceForUrl(url).get().resource; + final MetadataResource targetResource = cache.getTargetResourceForUrl(url).get().resource; + if (resourceType != 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( + || (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); - }); - for (var change : changes) { - 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")) { - // 3) 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")) { - originalValue = - Optional.of((new BeanWrapperImpl(sourceResource).getPropertyValue(path.get()))); - } - } catch (Exception e) { - // TODO: handle exception - // var message = e.getMessage(); - throw new InternalErrorException("Could not process path: " + path + ": " + e.getMessage()); - } - - // 4) Add a new operation to the ChangeLog - page.addOperation(type, path.orElse(null), newValue.orElse(null), originalValue.orElse(null)); + 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); 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; From 182e6fc7c125343907bf350291ea6b2fb384d729 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 11:10:18 -0800 Subject: [PATCH 06/22] Spotless --- .../common/HapiCreateChangelogProcessor.java | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) 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 index f713b5f404..1e762ccb82 100644 --- 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 @@ -97,9 +97,14 @@ public IBaseResource createChangelog( var sourceResource = cache.getSourceResourceForUrl(((MetadataResource) source).getUrl()); if (targetResource.isPresent() && sourceResource.isPresent()) { var targetAdapter = IAdapterFactory.forFhirVersion(FhirVersionEnum.R4) - .createKnowledgeArtifactAdapter(targetResource.get().resource); + .createKnowledgeArtifactAdapter(targetResource.get().resource); var diffParameters = hapiArtifactDiffProcessor.getArtifactDiff( - sourceResource.get().resource, targetResource.get().resource, true, true, cache, terminologyEndpoint); + 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); @@ -122,7 +127,8 @@ public IBaseResource createChangelog( return null; } - private DiffCache populateCache(IBaseResource source, Bundle sourceBundle, IBaseResource target, Bundle targetBundle) { + 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) { @@ -159,14 +165,18 @@ private void processChanges( 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 MetadataResource sourceResource = cache.getSourceResourceForUrl(url).get().resource; - final MetadataResource targetResource = cache.getTargetResourceForUrl(url).get().resource; + if (!wasPageAlreadyProcessed + && cache.getSourceResourceForUrl(url).isPresent() + && cache.getTargetResourceForUrl(url).isPresent()) { + final MetadataResource sourceResource = + cache.getSourceResourceForUrl(url).get().resource; + final MetadataResource targetResource = + cache.getTargetResourceForUrl(url).get().resource; if (resourceType != null) { // don't generate changeLog pages for non-grouper ValueSets if (resourceType.equals("ValueSet") - && ((sourceResource != null && !KnowledgeArtifactProcessor.isGrouper(sourceResource)) - || (targetResource != null && !KnowledgeArtifactProcessor.isGrouper(targetResource)))) { + && ((sourceResource != null && !KnowledgeArtifactProcessor.isGrouper(sourceResource)) + || (targetResource != null && !KnowledgeArtifactProcessor.isGrouper(targetResource)))) { return; } // 2) Generate a page for each resource pair based on ResourceType @@ -174,7 +184,7 @@ private void processChanges( 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); + (PlanDefinition) sourceResource, (PlanDefinition) targetResource); default -> changelog.addPage(sourceResource, targetResource, url); }); // 3) Process each change @@ -185,20 +195,23 @@ private void processChanges( } } - private void processChange(ChangeLog changelog, DiffCache cache, - ParametersParameterComponent change, MetadataResource sourceResource, - ChangeLog.Page 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) { + && !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")); + .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); @@ -207,8 +220,7 @@ private void processChange(ChangeLog changelog, DiffCache cache, // Parameters object try { if (originalValue.isEmpty() && !type.equals("insert") && sourceResource != null && path.isPresent()) { - originalValue = - Optional.of((new BeanWrapperImpl(sourceResource).getPropertyValue(path.get()))); + originalValue = Optional.of((new BeanWrapperImpl(sourceResource).getPropertyValue(path.get()))); } } catch (Exception e) { throw new InternalErrorException("Could not process path: " + path + ": " + e.getMessage()); From a8192367f33312368a6c3e22d9035f21338403a4 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 12:05:16 -0800 Subject: [PATCH 07/22] Refactor - sonar --- .../cr/common/CreateChangelogProcessor.java | 30 ++++++++++++++----- .../cqf/fhir/cr/crmi/TransformProperties.java | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) 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 index 0264a2df7b..b455487a58 100644 --- 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 @@ -134,7 +134,7 @@ public String getPageUrl(MetadataResource source, MetadataResource target) { private Optional getPriority(ValueSet valueSet) { return valueSet.getUseContext().stream() .filter(uc -> uc.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) - && uc.getCode().getCode().equals(TransformProperties.vsmPriorityCode)) + && uc.getCode().getCode().equals(TransformProperties.VSM_PRIORITY_CODE)) .findAny() .map(uc -> uc.getValueCodeableConcept().getCodingFirstRep().getCode()); } @@ -1011,14 +1011,14 @@ private void addOperationHandleUseContext(Object newValue, Object originalValue, String priorityToCheck = null; if (newValue instanceof UsageContext newUseContext && newUseContext.getCode().getSystem().equals(TransformProperties.usPHUsageContextType) - && newUseContext.getCode().getCode().equals(TransformProperties.vsmPriorityCode)) { + && 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.vsmPriorityCode)) { + && originalUseContext.getCode().getCode().equals(TransformProperties.VSM_PRIORITY_CODE)) { priorityToCheck = originalUseContext .getValueCodeableConcept() .getCodingFirstRep() @@ -1094,10 +1094,10 @@ public static class codeableConceptWithOperation { } public static class LibraryChild extends PageBase { - public ValueAndOperation purpose = new ValueAndOperation(); - public ValueAndOperation effectiveStart = new ValueAndOperation(); - public ValueAndOperation releaseDate = new ValueAndOperation(); - public List relatedArtifacts = new ArrayList<>(); + private final ValueAndOperation purpose = new ValueAndOperation(); + private final ValueAndOperation effectiveStart = new ValueAndOperation(); + private final ValueAndOperation releaseDate = new ValueAndOperation(); + private final List relatedArtifacts = new ArrayList<>(); LibraryChild( String name, @@ -1124,6 +1124,22 @@ public static class LibraryChild extends PageBase { } } + 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)) 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 998a0e70f6..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,7 +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 vsmPriorityCode = "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 = From 4aeca784fec557be19c630611e5dbcaad48523c6 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 12:39:27 -0800 Subject: [PATCH 08/22] Sonar refactor --- .../cr/common/CreateChangelogProcessor.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) 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 index b455487a58..de515c0b87 100644 --- 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 @@ -1058,24 +1058,44 @@ public static class OtherChild extends PageBase { } public static class RelatedArtifactUrlWithOperation extends ValueAndOperation { - public RelatedArtifact fullRelatedArtifact; - public List conditions = new ArrayList<>(); - public codeableConceptWithOperation priority = new codeableConceptWithOperation(null); + private final RelatedArtifact fullRelatedArtifact; + private List conditions = new ArrayList<>(); + private final CodeableConceptWithOperation priority = new CodeableConceptWithOperation(null); - public static class codeableConceptWithOperation { - public CodeableConcept value; - public Operation operation; + public RelatedArtifact getFullRelatedArtifact() { + return fullRelatedArtifact; + } + + public List getConditions() { + return conditions; + } + + public CodeableConceptWithOperation getPriority() { + return priority; + } - codeableConceptWithOperation(CodeableConcept e) { + 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())) + .map(e -> new CodeableConceptWithOperation((CodeableConcept) e.getValue())) .toList(); var priorities = relatedArtifact.getExtensionsByUrl(TransformProperties.vsmPriority).stream() .map(e -> (CodeableConcept) e.getValue()) From c0bc189bea59ce727bcfe306f6b0bd1da57179a3 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 12:47:26 -0800 Subject: [PATCH 09/22] Sonar refactor --- .../cr/common/CreateChangelogProcessor.java | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) 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 index de515c0b87..59f4dedc98 100644 --- 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 @@ -774,19 +774,63 @@ public void setOperation(Operation operation) { } public static class Leaf { - public String memberOid; - public String name; - public String title; - public String url; - public List codeSystems = new ArrayList(); - public String status; - public List conditions = new ArrayList(); - public ValueAndOperation priority = new ValueAndOperation(); - public Operation operation; + 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 { - public String name; - public String oid; + 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; From 60ab4316fc92d99ae7c392522a51a7b9bfef356b Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 14:52:34 -0800 Subject: [PATCH 10/22] Sonar refactor --- .../cr/common/CreateChangelogProcessor.java | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) 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 index 59f4dedc98..c8c1e08512 100644 --- 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 @@ -674,18 +674,62 @@ public ValueAndOperation getPriority() { } public static class Code { - public String id; - public String system; - public String code; - public String version; - public String display; - public String memberOid; - public String codeSystemOid; - public String codeSystemName; - public String parentValueSetName; - public String parentValueSetTitle; - public String parentValueSetUrl; - public Operation operation; + private final String id; + private final String system; + private final String code; + 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 getCode() { + return code; + } + + 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; + } Code( String id, From 65e4cff36e6685dbd082f8e6fd1b020f83bff8f5 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 15:32:59 -0800 Subject: [PATCH 11/22] Refactor --- .../fhir/cr/common/CreateChangelogProcessor.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index c8c1e08512..2310f41739 100644 --- 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 @@ -676,7 +676,7 @@ public ValueAndOperation getPriority() { public static class Code { private final String id; private final String system; - private final String code; + private final String codeValue; private final String version; private final String display; private final String memberOid; @@ -695,8 +695,8 @@ public String getSystem() { return system; } - public String getCode() { - return code; + public String getCodeValue() { + return codeValue; } public String getVersion() { @@ -748,7 +748,7 @@ public String getParentValueSetUrl() { this.codeSystemOid = getCodeSystemOid(system); this.codeSystemName = getCodeSystemName(system); } - this.code = code; + this.codeValue = code; this.version = version; this.display = display; this.memberOid = memberOid; @@ -762,7 +762,7 @@ public Code copy() { return new Code( this.id, this.system, - this.code, + this.codeValue, this.version, this.display, this.memberOid, @@ -917,7 +917,7 @@ public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { : coding.getDisplay(); final var maybeExisting = this.conditions.stream() .filter(code -> - code.system.equals(coding.getSystem()) && code.code.equals(coding.getCode())) + code.system.equals(coding.getSystem()) && code.codeValue.equals(coding.getCode())) .findAny(); if (maybeExisting.isEmpty()) { final var newCondition = new ValueSetChild.Code( @@ -1121,8 +1121,8 @@ private void updateCodeOperation(String codeToCheck, Operation operation) { if (codeToCheck != null) { final String codeNotNull = codeToCheck; this.codes.stream() - .filter(code -> code.code != null) - .filter(code -> code.code.equals(codeNotNull)) + .filter(code -> code.codeValue != null) + .filter(code -> code.codeValue.equals(codeNotNull)) .findAny() .ifPresentOrElse( code -> code.setOperation(operation), From 9b8f1f93e7fa77b7cb31c33b2c5bb131df7c73b8 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Mon, 23 Feb 2026 09:54:16 -0800 Subject: [PATCH 12/22] Cleanup remaining warnings and suppress where appropriate --- .../cr/common/CreateChangelogProcessor.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 index 2310f41739..57c9eb0744 100644 --- 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 @@ -34,6 +34,9 @@ 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); @@ -49,8 +52,9 @@ public IBaseResource createChangelog( return new Parameters(); } + @SuppressWarnings("rawtypes") public static class ChangeLog { - private List> pages; + 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"; @@ -63,11 +67,11 @@ public ChangeLog(String url) { this.manifestUrl = url; } - public List> getPages() { + public List getPages() { return pages; } - public void setPages(List> pages) { + public void setPages(List pages) { this.pages = pages; } @@ -369,7 +373,7 @@ public Page addPage(IBaseResource sourceResource, IBaseResource targ return page; } - public Optional> getPage(String url) { + public Optional getPage(String url) { return this.pages.stream() .filter(p -> p.url != null && p.url.equals(url)) .findAny(); @@ -383,11 +387,11 @@ public void handleRelatedArtifacts() { var manifestNewData = (LibraryChild) specLibrary.newData; if (manifestNewData != null) { for (final var page : this.pages) { - if (page.oldData instanceof ValueSetChild) { - updateConditionsAndPriorities(manifestOldData, (ValueSetChild) page.oldData); + if (page.oldData instanceof ValueSetChild oldValueSet) { + updateConditionsAndPriorities(manifestOldData, oldValueSet); } - if (page.newData instanceof ValueSetChild) { - updateConditionsAndPriorities(manifestNewData, (ValueSetChild) page.newData); + if (page.newData instanceof ValueSetChild newValueSet) { + updateConditionsAndPriorities(manifestNewData, newValueSet); } } } @@ -731,6 +735,7 @@ public String getParentValueSetUrl() { return parentValueSetUrl; } + @SuppressWarnings("java:S107") Code( String id, String system, @@ -939,6 +944,7 @@ public ValueSetChild.Code tryAddCondition(CodeableConcept condition) { } } + @SuppressWarnings("java:S107") ValueSetChild( String title, String id, @@ -1010,6 +1016,7 @@ public void addOperation(String type, String path, Object newValue, Object origi } } + @SuppressWarnings("unchecked") private void addOperationHandleCompose( String type, String path, Object newValue, Object originalValue, Operation operation) { // if the valuesets changed @@ -1083,14 +1090,15 @@ private void addOperationHandleExpansion( } } + @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) { - codeToCheck = ((ValueSet.ValueSetExpansionContainsComponent) originalValue).getCode(); + } else if (originalValue instanceof ValueSet.ValueSetExpansionContainsComponent originalVSECC) { + codeToCheck = originalVSECC.getCode(); } return codeToCheck; } @@ -1207,6 +1215,7 @@ public static class LibraryChild extends PageBase { private final ValueAndOperation releaseDate = new ValueAndOperation(); private final List relatedArtifacts = new ArrayList<>(); + @SuppressWarnings("java:S107") LibraryChild( String name, String purpose, From 1c563c7cb86134a353fa70350da869a9baca29e5 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 24 Feb 2026 10:12:51 -0800 Subject: [PATCH 13/22] Fix bug where added or removed resources would be excluded from changelog --- .../common/HapiCreateChangelogProcessor.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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 index 1e762ccb82..4267abfcfa 100644 --- 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 @@ -41,6 +41,7 @@ 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; @@ -166,13 +167,17 @@ private void processChanges( // 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 MetadataResource sourceResource = - cache.getSourceResourceForUrl(url).get().resource; - final MetadataResource targetResource = - cache.getTargetResourceForUrl(url).get().resource; + && (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)) From 0fe04635b8e7573d1b3740a29502eff894144239 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 24 Feb 2026 12:32:58 -0800 Subject: [PATCH 14/22] Add Changelog Tests --- .../common/HapiCreateChangelogProcessor.java | 3 +- .../HapiCreateChangelogProcessorTest.java | 586 +++++++++++ .../src/test/resources/small-diff-bundle.json | 588 +++++++++++ .../small-dxtc-modified-diff-bundle.json | 669 ++++++++++++ .../small-vsm-gen-grouper-bundle.json | 981 ++++++++++++++++++ 5 files changed, 2826 insertions(+), 1 deletion(-) create mode 100644 cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessorTest.java create mode 100644 cqf-fhir-cr-hapi/src/test/resources/small-diff-bundle.json create mode 100644 cqf-fhir-cr-hapi/src/test/resources/small-dxtc-modified-diff-bundle.json create mode 100644 cqf-fhir-cr-hapi/src/test/resources/small-vsm-gen-grouper-bundle.json 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 index 4267abfcfa..1f30cf3226 100644 --- 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 @@ -83,11 +83,12 @@ public IBaseResource createChangelog( targetBundle = (Bundle) packages.get(1).get(); service.shutdownNow(); } catch (InterruptedException | ExecutionException e) { - service.shutdownNow(); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } throw new UnprocessableEntityException(e.getMessage()); + } finally { + service.shutdown(); } // 2) Fill the cache with the bundle contents 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..a6ac9e8a66 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/common/HapiCreateChangelogProcessorTest.java @@ -0,0 +1,586 @@ +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; + + /* private Parameters createChangelogSetup() { + loadTransaction("small-diff-bundle.json"); + var bundle = (Bundle) loadTransaction("small-dxtc-modified-diff-bundle.json"); + var maybeLib = bundle.getEntry().stream().filter(entry -> entry.getResponse().getLocation().contains("Library")).findFirst(); + Parameters diffParams = new Parameters(); + diffParams.addParameter("source", specificationLibReference); + diffParams.addParameter("target", maybeLib.get().getResponse().getLocation()); + var endpoint = new Endpoint(); + endpoint.setAddress("https://cts.nlm.nih.gov/fhir"); + endpoint.addExtension("vsacUsername", new StringType("tahaattarismile")); + endpoint.addExtension("apiKey", new StringType("e071d986-0c68-4d06-95ee-00602a2bb748")); + diffParams.addParameter("target", maybeLib.get().getResponse().getLocation()); + // diffParams.addParameter().setName("terminologyEndpoint").setResource( endpoint); + return diffParams; + }*/ + + @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("code").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("code").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("code").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("code").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("code").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(source, target, 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("code").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 From dbde3d593d2d07f0bfd91993c75a3516fb3c9427 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 24 Feb 2026 12:51:53 -0800 Subject: [PATCH 15/22] Spotless --- .../common/HapiCreateChangelogProcessor.java | 4 +- .../HapiCreateChangelogProcessorTest.java | 559 ++++++++++-------- .../cr/common/CreateChangelogProcessor.java | 5 +- 3 files changed, 320 insertions(+), 248 deletions(-) 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 index 1f30cf3226..a024d79af7 100644 --- 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 @@ -189,8 +189,8 @@ private void processChanges( 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); + case "PlanDefinition" -> + changelog.addPage((PlanDefinition) sourceResource, (PlanDefinition) targetResource); default -> changelog.addPage(sourceResource, targetResource, url); }); // 3) Process each change 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 index a6ac9e8a66..2d8423ed6f 100644 --- 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 @@ -95,19 +95,19 @@ void create_changelog_codes() { 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"); + 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(); + .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(); + .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); @@ -116,46 +116,46 @@ void create_changelog_codes() { 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")); + 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")); + 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); @@ -164,21 +164,29 @@ void create_changelog_codes() { 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("code").asText()); + for (final var code : page.get("oldData").get("codes")) { + CodeAndOperation expectedOldCode = + oldCodes.get(code.get("code").asText()); assertNotNull(expectedOldCode); if (expectedOldCode.operation != null) { - assertEquals(expectedOldCode.operation, code.get("operation").get("type").asText()); - assertEquals(expectedOldCode.code, code.get("memberOid").asText()); + 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("code").asText()); + for (final var code : page.get("newData").get("codes")) { + CodeAndOperation expectedNewCode = + newCodes.get(code.get("code").asText()); assertNotNull(expectedNewCode); if (expectedNewCode.operation != null) { - assertEquals(expectedNewCode.operation, code.get("operation").get("type").asText()); - assertEquals(expectedNewCode.code, code.get("memberOid").asText()); + assertEquals( + expectedNewCode.operation, + code.get("operation").get("type").asText()); + assertEquals( + expectedNewCode.code, code.get("memberOid").asText()); } } } @@ -195,93 +203,65 @@ void create_changelog_conditions_and_priorities() { 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"); + 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(); + .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(); + .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) - ) - ) - ); + 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()))); @@ -290,47 +270,89 @@ void create_changelog_conditions_and_priorities() { 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(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"); + 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("code").asText())).findAny(); + for (final var condition : leaf.get("conditions")) { + Optional conditionInList = expectedConditions.stream() + .filter(c -> c.code != null + && c.code.equals( + condition.get("code").asText())) + .findAny(); assertTrue(conditionInList.isPresent()); if (conditionInList.get().operation != null) { - assertEquals(conditionInList.get().operation, condition.get("operation").get("type").asText()); + 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()); + 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()); + 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(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"); + 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("code").asText())).findAny(); + for (final var condition : leaf.get("conditions")) { + Optional conditionInList = expectedConditions.stream() + .filter(c -> c.code != null + && c.code.equals( + condition.get("code").asText())) + .findAny(); assertTrue(conditionInList.isPresent()); if (conditionInList.get().operation != null) { - assertEquals(conditionInList.get().operation, condition.get("operation").get("type").asText()); + 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()); + 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()); + assertEquals( + expectedPriority.operation, + leaf.get("priority") + .get("operation") + .get("type") + .asText()); } } } @@ -346,36 +368,34 @@ void create_changelog_grouped_leaf() { 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"); + 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(); + .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(); + .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", "" - ); + "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", "" - ); + "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()); @@ -383,25 +403,30 @@ void create_changelog_grouped_leaf() { 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")) { + 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()); + assertEquals( + expectedLeaf, + leaf.get("operation").get("type").asText()); } } assertTrue(page.get("newData").get("leafValuesets").isArray()); - for (final var leaf: page.get("newData").get("leafValuesets")) { + 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()); + assertEquals( + expectedLeaf, + leaf.get("operation").get("type").asText()); } } } } }); } + @Test void create_changelog_extracts_vs_name_and_url() { var repository = new InMemoryFhirRepository(FhirContext.forR4()); @@ -409,61 +434,80 @@ void create_changelog_extracts_vs_name_and_url() { 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"); + 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(); + .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(); + .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" - ); + "Diagnosis_ProblemTriggersforPublicHealthReporting", + "DiphtheriaDisordersSNOMED", + "AnkylosingSpondylitis", + "AcanthamoebaDiseaseKeratitisDisordersSNOMED"); var newLeafValueSetNames = List.of( - "Diagnosis_ProblemTriggersforPublicHealthReporting", - "AnkylosingSpondylitis", - "Cholera (Disorders) (SNOMED)", - "UpdatedName" - ); + "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())); + 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")) { + 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()); + 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")) { + 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")); + 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")); } } } @@ -472,44 +516,44 @@ void create_changelog_extracts_vs_name_and_url() { } @Test - void created_deleted_groupers_should_be_visible() throws Exception{ + 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 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"); + 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(); + .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(); + .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 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" - ); + "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); @@ -520,17 +564,28 @@ void created_deleted_groupers_should_be_visible() throws Exception{ 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(); + 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) { + 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")); + 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")) { + 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("code").asText())); @@ -538,8 +593,10 @@ void created_deleted_groupers_should_be_visible() throws Exception{ 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")) { + 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())); @@ -553,23 +610,36 @@ void created_deleted_groupers_should_be_visible() throws Exception{ 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(); + 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")); + 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")) { + 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("code").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")) { + 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())); } @@ -578,6 +648,7 @@ void created_deleted_groupers_should_be_visible() throws Exception{ 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/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 index 57c9eb0744..58390a2c05 100644 --- 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 @@ -474,8 +474,9 @@ public void addOperation(String type, String path, Object currentValue, Object o 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"); + default -> + throw new UnprocessableEntityException( + "Unknown type provided when adding an operation to the ChangeLog"); } } else { throw new UnprocessableEntityException( From fff5a28ebe7026e3e50fab9ffd30404e8a6f10b6 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 27 Feb 2026 09:18:42 -0800 Subject: [PATCH 16/22] Update changelog tests --- .../HapiCreateChangelogProcessorTest.java | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) 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 index 2d8423ed6f..f5c40be47d 100644 --- 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 @@ -23,22 +23,7 @@ class HapiCreateChangelogProcessorTest { public HapiCreateChangelogProcessor createChangelogProcessor; - - /* private Parameters createChangelogSetup() { - loadTransaction("small-diff-bundle.json"); - var bundle = (Bundle) loadTransaction("small-dxtc-modified-diff-bundle.json"); - var maybeLib = bundle.getEntry().stream().filter(entry -> entry.getResponse().getLocation().contains("Library")).findFirst(); - Parameters diffParams = new Parameters(); - diffParams.addParameter("source", specificationLibReference); - diffParams.addParameter("target", maybeLib.get().getResponse().getLocation()); - var endpoint = new Endpoint(); - endpoint.setAddress("https://cts.nlm.nih.gov/fhir"); - endpoint.addExtension("vsacUsername", new StringType("tahaattarismile")); - endpoint.addExtension("apiKey", new StringType("e071d986-0c68-4d06-95ee-00602a2bb748")); - diffParams.addParameter("target", maybeLib.get().getResponse().getLocation()); - // diffParams.addParameter().setName("terminologyEndpoint").setResource( endpoint); - return diffParams; - }*/ + public InMemoryFhirRepository repository; @Test void create_changelog_pages() { @@ -166,7 +151,7 @@ void create_changelog_codes() { assertTrue(page.get("oldData").get("codes").isArray()); for (final var code : page.get("oldData").get("codes")) { CodeAndOperation expectedOldCode = - oldCodes.get(code.get("code").asText()); + oldCodes.get(code.get("codeValue").asText()); assertNotNull(expectedOldCode); if (expectedOldCode.operation != null) { assertEquals( @@ -179,7 +164,7 @@ void create_changelog_codes() { assertTrue(page.get("newData").get("codes").isArray()); for (final var code : page.get("newData").get("codes")) { CodeAndOperation expectedNewCode = - newCodes.get(code.get("code").asText()); + newCodes.get(code.get("codeValue").asText()); assertNotNull(expectedNewCode); if (expectedNewCode.operation != null) { assertEquals( @@ -269,13 +254,13 @@ void create_changelog_conditions_and_priorities() { 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("leafValueSets").isArray()); assertTrue(page.get("oldData") .get("priority") .get("value") .asText() .equals("routine")); - for (final var leaf : page.get("oldData").get("leafValuesets")) { + for (final var leaf : page.get("oldData").get("leafValueSets")) { assertTrue(leaf.get("conditions").isArray()); var memberOid = leaf.get("memberOid").asText(); assertTrue(oldLeafsAndConditions.containsKey(memberOid)); @@ -286,7 +271,7 @@ void create_changelog_conditions_and_priorities() { Optional conditionInList = expectedConditions.stream() .filter(c -> c.code != null && c.code.equals( - condition.get("code").asText())) + condition.get("codeValue").asText())) .findAny(); assertTrue(conditionInList.isPresent()); if (conditionInList.get().operation != null) { @@ -312,13 +297,13 @@ void create_changelog_conditions_and_priorities() { .asText()); } } - assertTrue(page.get("newData").get("leafValuesets").isArray()); + 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")) { + for (final var leaf : page.get("newData").get("leafValueSets")) { assertTrue(leaf.get("conditions").isArray()); var memberOid = leaf.get("memberOid").asText(); assertTrue(newLeafsAndConditions.containsKey(memberOid)); @@ -329,7 +314,7 @@ void create_changelog_conditions_and_priorities() { Optional conditionInList = expectedConditions.stream() .filter(c -> c.code != null && c.code.equals( - condition.get("code").asText())) + condition.get("codeValue").asText())) .findAny(); assertTrue(conditionInList.isPresent()); if (conditionInList.get().operation != null) { @@ -402,8 +387,8 @@ void create_changelog_grouped_leaf() { 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")) { + 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()) { @@ -412,8 +397,8 @@ void create_changelog_grouped_leaf() { leaf.get("operation").get("type").asText()); } } - assertTrue(page.get("newData").get("leafValuesets").isArray()); - for (final var leaf : page.get("newData").get("leafValuesets")) { + 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()) { @@ -473,9 +458,9 @@ void create_changelog_extracts_vs_name_and_url() { 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")) { + 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") @@ -489,9 +474,9 @@ void create_changelog_extracts_vs_name_and_url() { .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")) { + 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") @@ -588,22 +573,22 @@ void created_deleted_groupers_should_be_visible() throws Exception { 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("code").asText())); + 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")) { + 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(source, target, null); + 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()); @@ -631,15 +616,15 @@ void created_deleted_groupers_should_be_visible() throws Exception { 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("code").asText())); + 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")) { + 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())); } From 482b567a48d726d22ddb0f5d35b3b7b3cbe8240c Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 12 Mar 2026 12:04:43 -0700 Subject: [PATCH 17/22] Replace HashMaps with ConcurrentHashMaps in ResourceMatchers --- .../cqf/fhir/utility/matcher/ResourceMatcherDSTU3.java | 6 +++--- .../opencds/cqf/fhir/utility/matcher/ResourceMatcherR4.java | 6 +++--- .../opencds/cqf/fhir/utility/matcher/ResourceMatcherR5.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) 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() { From 092f99eca193e1dfdea4e61a5cff578bb0c320c2 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 31 Mar 2026 13:00:03 -0700 Subject: [PATCH 18/22] Update test data Previous data had the same VS version in both source and target, though the target was missing the name. Repository search was returning them in seemingly random order, leading to intermittent failure of this test. Updated name and version of the target VS to make them distinct. --- .../common/HapiCreateChangelogProcessorTest.java | 2 +- .../resources/small-dxtc-modified-diff-bundle.json | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) 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 index f5c40be47d..4793758bf9 100644 --- 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 @@ -443,7 +443,7 @@ void create_changelog_extracts_vs_name_and_url() { "AcanthamoebaDiseaseKeratitisDisordersSNOMED"); var newLeafValueSetNames = List.of( "Diagnosis_ProblemTriggersforPublicHealthReporting", - "AnkylosingSpondylitis", + "AnkylosingSpondylitisUpdated", "Cholera (Disorders) (SNOMED)", "UpdatedName"); assertDoesNotThrow(() -> { 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 index 63b0930d49..4fcfe76661 100644 --- 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 @@ -145,7 +145,7 @@ } ], "type": "depends-on", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20200310" }, { "extension": [ @@ -312,7 +312,7 @@ "include": [ { "valueSet": [ - "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310" + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20200310" ] }, { @@ -354,7 +354,7 @@ } }, { - "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20180310", + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090|20200310", "resource": { "resourceType": "ValueSet", "id": "2.16.840.1.113883.3.464.1003.113.11.1090", @@ -370,8 +370,9 @@ "value": "urn:oid:2.16.840.1.113883.3.464.1003.113.11.1090" } ], - "version": "20180310", - "status": "active", + "version": "20200310", + "name":"AnkylosingSpondylitisUpdated", + "status": "active", "useContext": [ { "code": { @@ -422,7 +423,7 @@ "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" + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.113.11.1090&version=20200310" } }, { From bb42e0f53b7d9280e69338eb066cf70f4c38d4fa Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 31 Mar 2026 14:39:14 -0700 Subject: [PATCH 19/22] Apply Sonar suggestions --- .../fhir/cr/hapi/common/HapiCreateChangelogProcessor.java | 8 ++++---- .../cr/hapi/common/HapiCreateChangelogProcessorTest.java | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) 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 index a024d79af7..adbc6ae5d6 100644 --- 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 @@ -82,10 +82,10 @@ public IBaseResource createChangelog( 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(); - } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new UnprocessableEntityException(e.getMessage()); + } catch (ExecutionException e) { throw new UnprocessableEntityException(e.getMessage()); } finally { service.shutdown(); 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 index 4793758bf9..e66db3f855 100644 --- 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 @@ -23,7 +23,6 @@ class HapiCreateChangelogProcessorTest { public HapiCreateChangelogProcessor createChangelogProcessor; - public InMemoryFhirRepository repository; @Test void create_changelog_pages() { From ff2f99a9acd62e9294e53aa1801c18a024dadc27 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 2 Apr 2026 13:05:21 -0700 Subject: [PATCH 20/22] Refactor to use shared thread pool & improve exceptions Include static block to ensure graceful shutdown of shared thread pool --- .../common/HapiCreateChangelogProcessor.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) 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 index adbc6ae5d6..dde39958f7 100644 --- 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 @@ -26,6 +26,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; @@ -58,37 +59,49 @@ public class HapiCreateChangelogProcessor implements ICreateChangelogProcessor { private final HapiArtifactDiffProcessor hapiArtifactDiffProcessor; + private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10); + public HapiCreateChangelogProcessor(IRepository repository) { this.fhirVersion = repository.fhirContext().getVersion().getVersion(); this.packageProcessor = new PackageProcessor(repository); this.hapiArtifactDiffProcessor = new HapiArtifactDiffProcessor(repository); } + static { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + EXECUTOR_SERVICE.shutdown(); + if (!EXECUTOR_SERVICE.awaitTermination(30, TimeUnit.SECONDS)) { + EXECUTOR_SERVICE.shutdownNow(); + } + } catch (InterruptedException e) { + EXECUTOR_SERVICE.shutdownNow(); + Thread.currentThread().interrupt(); + } + })); + } + @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( + packages = EXECUTOR_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 e) { Thread.currentThread().interrupt(); - throw new UnprocessableEntityException(e.getMessage()); + throw new InternalErrorException(e.getMessage()); } catch (ExecutionException e) { - throw new UnprocessableEntityException(e.getMessage()); - } finally { - service.shutdown(); + throw new InternalErrorException(e.getMessage()); } // 2) Fill the cache with the bundle contents @@ -126,7 +139,7 @@ public IBaseResource createChangelog( return bin; } - return null; + throw new UnprocessableEntityException("Could not find source or target resource in cached package responses"); } private DiffCache populateCache( From eabf514f70daa2a63f4050b859bb6eac57113360 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 2 Apr 2026 13:09:18 -0700 Subject: [PATCH 21/22] Replace use of BeanWrapperImpl with ModelResolver --- .../fhir/cr/hapi/common/HapiCreateChangelogProcessor.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index dde39958f7..d92b31986a 100644 --- 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 @@ -49,7 +49,7 @@ 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; +import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; @SuppressWarnings("UnstableApiUsage") public class HapiCreateChangelogProcessor implements ICreateChangelogProcessor { @@ -239,7 +239,8 @@ private void processChange( // Parameters object try { if (originalValue.isEmpty() && !type.equals("insert") && sourceResource != null && path.isPresent()) { - originalValue = Optional.of((new BeanWrapperImpl(sourceResource).getPropertyValue(path.get()))); + originalValue = Optional.of(FhirModelResolverCache.resolverForVersion(fhirVersion) + .resolvePath(sourceResource, path.get())); } } catch (Exception e) { throw new InternalErrorException("Could not process path: " + path + ": " + e.getMessage()); From 2121278da01d701984a8a006ab5afca2588afb34 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 2 Apr 2026 13:10:23 -0700 Subject: [PATCH 22/22] Make ChangelogProcessor version agnostic & extract inner classes to crmi package --- .../common/HapiCreateChangelogProcessor.java | 5 +- .../cr/common/CreateChangelogProcessor.java | 1370 +---------------- .../cqf/fhir/cr/crmi/changelog/ChangeLog.java | 383 +++++ .../fhir/cr/crmi/changelog/LibraryChild.java | 173 +++ .../cqf/fhir/cr/crmi/changelog/Operation.java | 55 + .../fhir/cr/crmi/changelog/OtherChild.java | 8 + .../cqf/fhir/cr/crmi/changelog/Page.java | 83 + .../cqf/fhir/cr/crmi/changelog/PageBase.java | 74 + .../crmi/changelog/PlanDefinitionChild.java | 8 + .../RelatedArtifactUrlWithOperation.java | 71 + .../cr/crmi/changelog/ValueAndOperation.java | 33 + .../fhir/cr/crmi/changelog/ValueSetChild.java | 501 ++++++ .../cqf/fhir/cr/library/LibraryProcessor.java | 3 +- 13 files changed, 1408 insertions(+), 1359 deletions(-) create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ChangeLog.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/LibraryChild.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Operation.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/OtherChild.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Page.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PageBase.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PlanDefinitionChild.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/RelatedArtifactUrlWithOperation.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueAndOperation.java create mode 100644 cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueSetChild.java 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 index d92b31986a..f732c22821 100644 --- 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 @@ -43,10 +43,11 @@ 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.cr.crmi.changelog.ChangeLog; +import org.opencds.cqf.fhir.cr.crmi.changelog.Page; import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; @@ -219,7 +220,7 @@ private void processChange( DiffCache cache, ParametersParameterComponent change, MetadataResource sourceResource, - ChangeLog.Page page) { + Page page) { if (change.hasName() && !change.getName().equals("operation") && change.hasResource() 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 index 58390a2c05..77059cb4fc 100644 --- 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 @@ -1,1374 +1,32 @@ 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 ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.repository.IRepository; +import org.hl7.fhir.instance.model.api.IBaseParameters; 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.opencds.cqf.fhir.utility.Resources; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; 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") +@SuppressWarnings("UnstableApiUsage") public class CreateChangelogProcessor implements ICreateChangelogProcessor { private static final Logger logger = LoggerFactory.getLogger(CreateChangelogProcessor.class); + private final FhirVersionEnum fhirVersion; + private final IAdapterFactory adapterFactory; - public CreateChangelogProcessor() { - /* Empty as we will not perform create changelog outside HAPI context */ + public CreateChangelogProcessor(IRepository repository) { + this.fhirVersion = repository.fhirContext().getVersion().getVersion(); + this.adapterFactory = IAdapterFactory.forFhirVersion(fhirVersion); } @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); - } - } - } - } + return adapterFactory + .createParameters((IBaseParameters) Resources.newBaseForVersion("Parameters", this.fhirVersion)) + .get(); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ChangeLog.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ChangeLog.java new file mode 100644 index 0000000000..d3c882e986 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ChangeLog.java @@ -0,0 +1,383 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import java.util.*; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.PlanDefinition; +import org.hl7.fhir.r4.model.ValueSet; +import org.opencds.cqf.fhir.cr.common.ArtifactDiffProcessor; +import org.opencds.cqf.fhir.cr.crmi.TransformProperties; +import org.opencds.cqf.fhir.utility.Canonicals; + +@SuppressWarnings("rawtypes") +public 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, ArtifactDiffProcessor.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, + ArtifactDiffProcessor.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, + ArtifactDiffProcessor.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 = ValueSetChild.Code.getCodeSystemName(system); + var codeSystemOid = ValueSetChild.Code.getCodeSystemOid(system); + var doesOidExistInList = leafData.getCodeSystems().stream() + .anyMatch(nameAndOid -> + nameAndOid.getOid() != null && nameAndOid.getOid().equals(codeSystemOid)); + if (!doesOidExistInList) { + leafData.getCodeSystems().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.getUrl() != null && p.getUrl().equals(url)) + .findAny(); + } + + public void handleRelatedArtifacts() { + var manifest = this.getPage(this.manifestUrl); + if (manifest.isPresent()) { + var specLibrary = manifest.get(); + var manifestOldData = (LibraryChild) specLibrary.getOldData(); + var manifestNewData = (LibraryChild) specLibrary.getNewData(); + if (manifestNewData != null) { + for (final var page : this.pages) { + if (page.getOldData() instanceof ValueSetChild oldValueSet) { + updateConditionsAndPriorities(manifestOldData, oldValueSet); + } + if (page.getNewData() instanceof ValueSetChild newValueSet) { + updateConditionsAndPriorities(manifestNewData, newValueSet); + } + } + } + } + } + + private void updateConditionsAndPriorities(LibraryChild manifestData, ValueSetChild pageData) { + for (final var ra : manifestData.getRelatedArtifacts()) { + pageData.getLeafValueSets().stream() + .filter(leafValueSet -> leafValueSet.getMemberOid() != null + && leafValueSet.getMemberOid().equals(Canonicals.getIdPart(ra.getValue()))) + .forEach(leafValueSet -> { + updateConditions(ra, leafValueSet); + updatePriorities(ra, leafValueSet); + }); + } + } + + private void updateConditions(RelatedArtifactUrlWithOperation ra, ValueSetChild.Leaf leafValueSet) { + ra.getConditions().forEach(condition -> { + if (condition.getValue() != null) { + var c = leafValueSet.tryAddCondition(condition.getValue()); + c.setOperation(condition.getOperation()); + } + }); + } + + private void updatePriorities(RelatedArtifactUrlWithOperation ra, ValueSetChild.Leaf leafValueSet) { + if (ra.getPriority().getValue() != null) { + var coding = ra.getPriority().getValue().getCodingFirstRep(); + leafValueSet.getPriority().setValue(coding.getCode()); + leafValueSet.getPriority().setOperation(ra.getPriority().getOperation()); + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/LibraryChild.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/LibraryChild.java new file mode 100644 index 0000000000..5c2dbbe15d --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/LibraryChild.java @@ -0,0 +1,173 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.RelatedArtifact; +import org.opencds.cqf.fhir.cr.crmi.TransformProperties; + +public 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.setValue(purpose); + } + if (!StringUtils.isEmpty(effectiveStart)) { + this.effectiveStart.setValue(effectiveStart); + } + if (!StringUtils.isEmpty(releaseDate)) { + this.releaseDate.setValue(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.getConditions().stream() + .filter(e -> e.getValue() + .getCodingFirstRep() + .getSystem() + .equals(((CodeableConcept) maybeCondition.getValue()) + .getCodingFirstRep() + .getSystem()) + && e.getValue() + .getCodingFirstRep() + .getCode() + .equals(((CodeableConcept) maybeCondition.getValue()) + .getCodingFirstRep() + .getCode())) + .findAny() + .ifPresent(condition -> condition.setOperation(newOperation)); + } + } + + private void tryAddPriorityOperation( + Extension maybePriority, RelatedArtifactUrlWithOperation target, Operation newOperation) { + if (maybePriority.getUrl().equals(TransformProperties.vsmPriority) + && (target.getPriority().getValue() != null + && target.getPriority() + .getValue() + .getCodingFirstRep() + .getSystem() + .equals(((CodeableConcept) maybePriority.getValue()) + .getCodingFirstRep() + .getSystem()) + && target.getPriority() + .getValue() + .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.setType(ChangeLog.REPLACE); + target.getPriority().setOperation(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() + .getFullRelatedArtifact() + .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/crmi/changelog/Operation.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Operation.java new file mode 100644 index 0000000000..6fd1983346 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Operation.java @@ -0,0 +1,55 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +public 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; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/OtherChild.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/OtherChild.java new file mode 100644 index 0000000000..ca8a896101 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/OtherChild.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +public class OtherChild extends PageBase { + + OtherChild(String title, String id, String version, String name, String url, String fhirType) { + super(title, id, version, name, url, fhirType); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Page.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Page.java new file mode 100644 index 0000000000..6525d9936d --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/Page.java @@ -0,0 +1,83 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; + +public 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 ChangeLog.REPLACE -> addReplaceOperation(type, path, currentValue, originalValue); + case ChangeLog.DELETE -> addDeleteOperation(type, path, originalValue); + case ChangeLog.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(ChangeLog.INSERT)) { + throw new UnprocessableEntityException(ChangeLog.WRONG_TYPE); + } + this.newData.addOperation(type, path, currentValue, null); + } + + void addDeleteOperation(String type, String path, Object originalValue) { + if (!type.equals(ChangeLog.DELETE)) { + throw new UnprocessableEntityException(ChangeLog.WRONG_TYPE); + } + this.oldData.addOperation(type, path, null, originalValue); + } + + void addReplaceOperation(String type, String path, Object currentValue, Object originalValue) { + if (!type.equals(ChangeLog.REPLACE)) { + throw new UnprocessableEntityException(ChangeLog.WRONG_TYPE); + } + this.oldData.addOperation(type, path, currentValue, null); + this.newData.addOperation(type, path, null, originalValue); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PageBase.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PageBase.java new file mode 100644 index 0000000000..11b1777d65 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PageBase.java @@ -0,0 +1,74 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import org.apache.commons.lang3.StringUtils; + +public 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.setValue(title); + } + if (!StringUtils.isEmpty(id)) { + this.id.setValue(id); + } + if (!StringUtils.isEmpty(version)) { + this.version.setValue(version); + } + if (!StringUtils.isEmpty(name)) { + this.name.setValue(name); + } + if (!StringUtils.isEmpty(url)) { + this.url.setValue(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); + } + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PlanDefinitionChild.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PlanDefinitionChild.java new file mode 100644 index 0000000000..653affc313 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/PlanDefinitionChild.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +public class PlanDefinitionChild extends PageBase { + + PlanDefinitionChild(String title, String id, String version, String name, String url) { + super(title, id, version, name, url, "PlanDefinition"); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/RelatedArtifactUrlWithOperation.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/RelatedArtifactUrlWithOperation.java new file mode 100644 index 0000000000..0be60d516a --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/RelatedArtifactUrlWithOperation.java @@ -0,0 +1,71 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import java.util.ArrayList; +import java.util.List; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.RelatedArtifact; +import org.opencds.cqf.fhir.cr.crmi.TransformProperties; + +public 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; + } + + public void setOperation(Operation operation) { + this.operation = 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; + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueAndOperation.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueAndOperation.java new file mode 100644 index 0000000000..6a1f482491 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueAndOperation.java @@ -0,0 +1,33 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; + +public 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.getType().equals(operation.getType()) + && this.operation.getPath().equals(operation.getPath()) + && this.operation.getNewValue() != operation.getNewValue()) { + throw new UnprocessableEntityException("Multiple changes to the same element"); + } + this.operation = operation; + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueSetChild.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueSetChild.java new file mode 100644 index 0000000000..fb441a8794 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/crmi/changelog/ValueSetChild.java @@ -0,0 +1,501 @@ +package org.opencds.cqf.fhir.cr.crmi.changelog; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.PrimitiveType; +import org.hl7.fhir.r4.model.UsageContext; +import org.hl7.fhir.r4.model.ValueSet; +import org.opencds.cqf.fhir.cr.crmi.TransformProperties; +import org.opencds.cqf.fhir.utility.Canonicals; + +public 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.getType().equals(operation.getType()) + && this.operation.getPath().equals(operation.getPath()) + && this.operation.getNewValue() != operation.getNewValue()) { + 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 Leaf.NameAndOid copy() { + return new Leaf.NameAndOid(this.name, this.oid); + } + } + + Leaf(String memberOid, String name, String title, String url, Enumerations.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(Leaf.NameAndOid::copy).collect(Collectors.toList()); + copy.conditions = this.conditions.stream().map(Code::copy).collect(Collectors.toList()); + copy.priority = new ValueAndOperation(); + copy.priority.setValue(this.priority.getValue()); + copy.priority.setOperation(this.priority.getOperation()); + copy.operation = this.operation; + return copy; + } + + public 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 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(ValueSet.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.setValue(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(ValueSet.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(ChangeLog.REPLACE) ? originalValue : null)) + .toList(); + } else if (originalValue instanceof ValueSet.ValueSetComposeComponent originalVSCC + && originalVSCC.getIncludeFirstRep().hasValueSet()) { + urlsToCheck = originalVSCC.getInclude().stream() + .filter(ValueSet.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(ChangeLog.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.setOperation(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)); + } + } +} 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 767669c6ae..663c4d9989 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 @@ -296,7 +296,8 @@ public , R extends IBaseResource> IBaseResource Either3 sourceLibrary, Either3 targetLibrary, IBaseResource terminologyEndpoint) { - var processor = createChangelogProcessor != null ? createChangelogProcessor : new CreateChangelogProcessor(); + var processor = + createChangelogProcessor != null ? createChangelogProcessor : new CreateChangelogProcessor(repository); return processor.createChangelog( resolveLibrary(sourceLibrary), resolveLibrary(targetLibrary), terminologyEndpoint); }