From 9c18b2f1f04ab9d66bfbd396d19dbe162962a53a Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Mon, 2 Feb 2026 12:19:01 -0800 Subject: [PATCH 01/15] 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 000000000..228d1ed07 --- /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 56a531650..c05f00db7 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 f03be6bf0..72315ecbe 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 000000000..36086aae0 --- /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 000000000..bd6186197 --- /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 000000000..8c53e6501 --- /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 000000000..c4c8c4e31 --- /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 2e0c99bda..6cb00dfb4 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 d46978646174faa49b2ad278ee8bc6d960b83347 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 5 Feb 2026 09:57:28 -0800 Subject: [PATCH 02/15] 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 228d1ed07..6bd64fa97 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 8c53e6501..22ac23ba5 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 42478d6f3..998a0e70f 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 f302113f1ae2deedeaa89f7aed9527e07cb13e86 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 19 Feb 2026 10:58:09 -0800 Subject: [PATCH 03/15] 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 6bd64fa97..3cfd90942 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 22ac23ba5..d8a09069c 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 c4c8c4e31..62b343b47 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 6cb00dfb4..767669c6a 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 a393860d8a4bc937e4e0f3a5b63e49f9c831de9c Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Thu, 19 Feb 2026 11:28:24 -0800 Subject: [PATCH 04/15] 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 d8a09069c..0264a2df7 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 3bcd2417752ce528bac9b094fb80bd7c992f8412 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 11:07:53 -0800 Subject: [PATCH 05/15] 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 3cfd90942..f713b5f40 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 f64c42ba5..c9d7f8da3 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 685826daccf8990870e96210ecfd536332c42a96 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 11:10:18 -0800 Subject: [PATCH 06/15] 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 f713b5f40..1e762ccb8 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 51c581bfccedbe933b62e7471825977bcab7200c Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 12:05:16 -0800 Subject: [PATCH 07/15] 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 0264a2df7..b455487a5 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 998a0e70f..fcf24ed10 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 8a8731a0750f36dda84b4f6d6721305c82fb0102 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 12:39:27 -0800 Subject: [PATCH 08/15] 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 b455487a5..de515c0b8 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 12024ce329268011af417c48166e8bc8da45bebf Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 12:47:26 -0800 Subject: [PATCH 09/15] 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 de515c0b8..59f4dedc9 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 ed42c297ee7c7427f2301dc615afba3f377c4da1 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 14:52:34 -0800 Subject: [PATCH 10/15] 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 59f4dedc9..c8c1e0851 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 9ddeedfc929f9307c9f1aa2048e0d13c182650df Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Fri, 20 Feb 2026 15:32:59 -0800 Subject: [PATCH 11/15] 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 c8c1e0851..2310f4173 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 9fc6f22eb2942d2f95fcf68f6911fbdd4bbeca26 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Mon, 23 Feb 2026 09:54:16 -0800 Subject: [PATCH 12/15] 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 2310f4173..57c9eb074 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 ec333fc417e5e3a431d12a088e8555481421a3cf Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 24 Feb 2026 10:12:51 -0800 Subject: [PATCH 13/15] 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 1e762ccb8..4267abfcf 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 b3e314db4c9aadfbac17829efd0ddfce8a46d043 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 24 Feb 2026 12:32:58 -0800 Subject: [PATCH 14/15] 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 4267abfcf..1f30cf322 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 000000000..a6ac9e8a6 --- /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 000000000..b221ed2f4 --- /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 000000000..63b0930d4 --- /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 000000000..ff6283787 --- /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 2b5d6092c12e6e70836c098c7d0c12920b236913 Mon Sep 17 00:00:00 2001 From: Chris O Riordan Date: Tue, 24 Feb 2026 12:51:53 -0800 Subject: [PATCH 15/15] 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 1f30cf322..a024d79af 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 a6ac9e8a6..2d8423ed6 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 57c9eb074..58390a2c0 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(