From 83f8ead36bb63b242f61efc1336b185b834322e0 Mon Sep 17 00:00:00 2001 From: Jonathan Percival Date: Thu, 25 Jan 2024 11:39:53 -0700 Subject: [PATCH] Cleanup and a test --- .../StripGeneratedContentOperation.java | 13 +- .../stripcontent/BaseContentStripper.java | 207 +++++++++++++++++ .../stripcontent/BaseStripContent.java | 213 ------------------ .../stripcontent/ContentStripper.java | 8 + ...ntDstu3.java => ContentStripperDstu3.java} | 4 +- .../stripcontent/ContentStripperOptions.java | 62 +++++ ...pContentR4.java => ContentStripperR4.java} | 4 +- ...pContentR5.java => ContentStripperR5.java} | 5 +- .../stripcontent/IStripContent.java | 7 - .../stripcontent/StripContentExecutor.java | 82 +++++++ .../stripcontent/StripContentParams.java | 40 ++-- .../stripcontent/StripContentProcessor.java | 70 ------ .../StripGeneratedContentOperationTest.java | 50 ++-- 13 files changed, 433 insertions(+), 332 deletions(-) create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseContentStripper.java delete mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseStripContent.java create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripper.java rename tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/{StripContentDstu3.java => ContentStripperDstu3.java} (75%) create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperOptions.java rename tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/{StripContentR4.java => ContentStripperR4.java} (75%) rename tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/{StripContentR5.java => ContentStripperR5.java} (67%) delete mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/IStripContent.java create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentExecutor.java delete mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentProcessor.java diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperation.java index 9e7946edd..7570cd384 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperation.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperation.java @@ -2,7 +2,7 @@ import org.opencds.cqf.tooling.Operation; import org.opencds.cqf.tooling.operations.stripcontent.StripContentParams; -import org.opencds.cqf.tooling.operations.stripcontent.StripContentProcessor; +import org.opencds.cqf.tooling.operations.stripcontent.StripContentExecutor; public class StripGeneratedContentOperation extends Operation { @Override @@ -20,21 +20,24 @@ public void execute(String[] args) { switch (flag.replace("-", "").toLowerCase()) { case "outputpath": case "op": - params.outputPath(value); + params.outputDirectory(value); break; case "pathtores": case "ptr": - params.inputPath(value); + params.inputDirectory(value); break; case "version": case "v": params.version(value); break; + + case "cql": + params.cqlExportDirectory(value); + break; default: throw new IllegalArgumentException("Unknown flag: " + flag); } } - var processor = new StripContentProcessor(params); - processor.execute(); + new StripContentExecutor(params).execute(); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseContentStripper.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseContentStripper.java new file mode 100644 index 000000000..e71c03657 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseContentStripper.java @@ -0,0 +1,207 @@ +package org.opencds.cqf.tooling.operations.stripcontent; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.Attachment; +import org.hl7.fhir.r5.model.DomainResource; +import org.hl7.fhir.r5.model.Extension; +import org.hl7.fhir.r5.model.Library; +import org.hl7.fhir.r5.model.Measure; +import org.hl7.fhir.r5.model.Parameters; +import org.hl7.fhir.r5.model.PlanDefinition; +import org.hl7.fhir.r5.model.Questionnaire; +import org.hl7.fhir.r5.model.RelatedArtifact; +import org.hl7.fhir.r5.model.Resource; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; + +/** + * This class is used to strip autogenerated content from FHIR resources. This includes narrative, + * extensions added by the tooling, related artifacts that are auto detected from the CQL, + * contained resources added by the tooling, and ELM generated from the CQL. + * + * This class converts the resource to its R5 equivalent, strips the content, and then converts + * back to the original FHIR version. + * + * The T parameter is used to specify the version of the Resource base class to use for the operation + * and conversions. + */ +abstract class BaseContentStripper implements ContentStripper { + protected abstract FhirContext context(); + protected abstract Resource convertToR5(T resource); + protected abstract T convertFromR5(Resource resource); + + @SuppressWarnings("unchecked") + public void stripFile(File inputFile, File outputFile, ContentStripperOptions options) { + var resource = parseResource(inputFile); + var upgraded = convertToR5((T)resource); + stripResource(upgraded, outputFile, options); + var downgraded = convertFromR5(upgraded); + writeResource(outputFile, downgraded); + } + + protected void writeContent(File f, String content) { + if (!f.getParentFile().exists()) { + f.getParentFile().mkdirs(); + } + + try (var writer = new BufferedWriter(new FileWriter(f))) { + writer.write(content); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected IParser parserForFile(File file) { + if (file.getName().endsWith(".json")) { + return context().newJsonParser(); + } else if (file.getName().endsWith(".xml")) { + return context().newXmlParser(); + } else { + throw new IllegalArgumentException(String.format("unsupported file type: %s", file.getName())); + } + } + + protected IBaseResource parseResource(File file) { + var parser = parserForFile(file); + try (var reader = new FileReader(file)) { + return parser.parseResource(reader); + } + catch (IOException | DataFormatException e) { + throw new RuntimeException(String.format("Error parsing file %s", file.getName()), e); + } + } + + protected void writeResource(File file, IBaseResource resource) { + var parser = parserForFile(file).setPrettyPrint(true); + var output = parser.encodeResourceToString(resource); + writeContent(file, output); + } + + // Output file is required because the CQL export functionality requires knowledge of the library + // file location to correctly set the Library.content.url property. + private Resource stripResource(IBaseResource resource, File outputFile, ContentStripperOptions options) { + switch (resource.fhirType()) { + case "Library": + return stripLibrary((Library) resource, outputFile, options); + case "Measure": + return stripMeasure((Measure) resource, options); + case "PlanDefinition": + return stripPlanDefinition((PlanDefinition) resource, options); + case "Questionnaire": + return stripQuestionnaire((Questionnaire) resource, options); + default: + return stripResource((DomainResource) resource, options); + } + } + + private boolean isCqlOptionsParameters(Resource resource) { + if (!(resource instanceof Parameters)) { + return false; + } + + var parameters = (Parameters) resource; + return "options".equals(parameters.getId()); + } + + private List filterContained(List contained) { + return contained.stream() + .filter(this::isCqlOptionsParameters) + .collect(Collectors.toList()); + } + + private List filterExtensions(List extensions, Set strippedExtensions) { + return extensions.stream() + .filter(extension -> !strippedExtensions.contains(extension.getUrl())) + .collect(Collectors.toList()); + } + + private List filterContent(List attachments, Set strippedContentTypes) { + return attachments.stream() + .filter(attachment -> !strippedContentTypes.contains(attachment.getContentType())) + .collect(Collectors.toList()); + } + + private List filterRelatedArtifacts(List artifacts) { + return artifacts + .stream() + .filter(x -> !RelatedArtifact.RelatedArtifactType.DEPENDSON.equals(x.getType())) + .collect(Collectors.toList()); + } + + // Strip library includes functionality to export the cql file, + // so it requires knowledge of the target directory for the Library. + private Library stripLibrary(Library library, File libraryFile, ContentStripperOptions options) { + stripResource(library, options); + library.setParameter(null); + library.setDataRequirement(null); + library.setRelatedArtifact(filterRelatedArtifacts(library.getRelatedArtifact())); + library.setContent(filterContent(library.getContent(), options.strippedContentTypes())); + exportCql(library.getContent(), library.getName(), libraryFile, options.cqlExportDirectory()); + return library; + } + + private Measure stripMeasure(Measure measure, ContentStripperOptions options) { + stripResource(measure, options); + measure.setRelatedArtifact(filterRelatedArtifacts(measure.getRelatedArtifact())); + return measure; + } + + private PlanDefinition stripPlanDefinition(PlanDefinition planDefinition, ContentStripperOptions options) { + stripResource(planDefinition, options); + planDefinition.setRelatedArtifact(filterRelatedArtifacts(planDefinition.getRelatedArtifact())); + return planDefinition; + } + + private Questionnaire stripQuestionnaire(Questionnaire questionnaire, ContentStripperOptions options) { + stripResource(questionnaire, options); + return questionnaire; + } + + private DomainResource stripResource(DomainResource resource, ContentStripperOptions options) { + resource.setText(null); + resource.setExtension(filterExtensions(resource.getExtension(), options.strippedExtensionUrls())); + resource.setContained(filterContained(resource.getContained())); + return resource; + } + + private void exportCql(Attachment content, String libraryName, File libraryFile, File cqlExportDirectory) { + checkNotNull(libraryName, "libraryName must be provided"); + if (content.getData() == null || cqlExportDirectory == null) { + return; + } + + // CQL content is encoded as base64, so we need to decode it + // to get back to the original CQL. + var base64 = content.getDataElement().getValueAsString(); + var cql = new String(java.util.Base64.getDecoder().decode(base64)); + + var cqlFileName = libraryName + ".cql"; + var cqlFile = cqlExportDirectory.toPath().resolve(cqlFileName).toFile(); + + content.setUrl(libraryFile.toPath().relativize(cqlFile.toPath()).toString()); + content.setDataElement(null); + writeContent(cqlFile, cql); + } + + private void exportCql(List content, String libraryName, File libraryOutputFile, File cqlExportDirectory) { + for (Attachment attachment : content) { + if (ContentStripperOptions.CQL_CONTENT_TYPE.equals(attachment.getContentType())) { + exportCql(attachment, libraryName, libraryOutputFile, cqlExportDirectory); + } + } + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseStripContent.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseStripContent.java deleted file mode 100644 index ac6a39d75..000000000 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseStripContent.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.opencds.cqf.tooling.operations.stripcontent; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r5.model.Attachment; -import org.hl7.fhir.r5.model.DomainResource; -import org.hl7.fhir.r5.model.Extension; -import org.hl7.fhir.r5.model.Library; -import org.hl7.fhir.r5.model.Measure; -import org.hl7.fhir.r5.model.Parameters; -import org.hl7.fhir.r5.model.PlanDefinition; -import org.hl7.fhir.r5.model.Questionnaire; -import org.hl7.fhir.r5.model.RelatedArtifact; -import org.hl7.fhir.r5.model.Resource; - -import ca.uhn.fhir.context.FhirContext; - -abstract class BaseStripContent implements IStripContent { - protected static final Set STRIPPED_CONTENT_TYPES = new HashSet<>( - Arrays.asList("application/elm+xml", "application/elm+json")); - protected static final String CQL_CONTENT_TYPE = "text/cql"; - protected static final Set STRIPPED_EXTENSION_URLS = new HashSet<>( - Arrays.asList("http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-parameter", - "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-dataRequirement", - "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-logicDefinition", - "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-softwaresystem", - "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", - "http://hl7.org/fhir/StructureDefinition/cqf-cqlOptions")); - - protected abstract FhirContext context(); - - protected abstract Resource convertToR5(T resource); - protected abstract T convertFromR5(Resource resource); - - @SuppressWarnings("unchecked") - public void stripFile(File inputFile, File outputFile) { - var resource = parseResource(inputFile); - var upgraded = convertToR5((T)resource); - stripResource(upgraded, outputFile); - var downgraded = convertFromR5(upgraded); - writeFile(outputFile, downgraded); - } - - protected void writeFile(File f, String content) { - if (!f.getParentFile().exists()) { - f.getParentFile().mkdirs(); - } - - try (var writer = new BufferedWriter(new FileWriter(f))) { - writer.write(content); - writer.flush(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected IBaseResource parseResource(File file) { - IBaseResource theResource = null; - try { - if (file.getName().endsWith(".json")) { - theResource = context().newJsonParser().parseResource(new FileReader(file)); - } else if(file.getName().endsWith(".xml")){ - theResource = context().newXmlParser().parseResource(new FileReader(file)); - } - - if (theResource == null) { - throw new RuntimeException(String.format("failed to parse resource for file: %s", file.toString())); - } - - return theResource; - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - } - - protected void writeFile(File f, IBaseResource resource) { - String output = ""; - if (f.getName().endsWith(".json")) { - output = context().newJsonParser().setPrettyPrint(true).encodeResourceToString(resource); - } else if (f.getName().endsWith(".xml")) { - output = context().newXmlParser().setPrettyPrint(true).encodeResourceToString(resource); - } - - writeFile(f, output); - } - - private Resource stripResource(IBaseResource resource, File outputFile) { - switch (resource.fhirType()) { - case "Library": - return stripLibrary((Library) resource, outputFile); - case "Measure": - return stripMeasure((Measure) resource); - case "PlanDefinition": - return stripPlanDefinition((PlanDefinition) resource); - case "Questionnaire": - return stripQuestionnaire((Questionnaire) resource); - default: - return stripResource((DomainResource) resource); - } - } - - private boolean isCqlOptionsParameters(Resource resource) { - if (!(resource instanceof Parameters)) { - return false; - } - - var parameters = (Parameters) resource; - return "options".equals(parameters.getId()); - } - - private List filterContained(List contained) { - return contained.stream() - .filter(this::isCqlOptionsParameters) - .collect(Collectors.toList()); - } - - private List filterExtensions(List extensions) { - return extensions.stream() - .filter(extension -> !STRIPPED_EXTENSION_URLS.contains(extension.getUrl())) - .collect(Collectors.toList()); - } - - private List filterContent(List attachments) { - return attachments.stream() - .filter(attachment -> !STRIPPED_CONTENT_TYPES.contains(attachment.getContentType())) - .collect(Collectors.toList()); - } - - private List filterRelatedArtifacts(List artifacts) { - return artifacts - .stream() - .filter(x -> !RelatedArtifact.RelatedArtifactType.DEPENDSON.equals(x.getType())) - .collect(Collectors.toList()); - } - - // Strip library includes functionality to export the cql file, - // so it requires knowledge of the target directory for the Library. - private Library stripLibrary(Library library, File outputFile) { - library.setText(null); - library.setParameter(null); - library.setDataRequirement(null); - library.setContained(filterContained(library.getContained())); - library.setRelatedArtifact(filterRelatedArtifacts(library.getRelatedArtifact())); - library.setExtension(filterExtensions(library.getExtension())); - library.setContent(filterContent(library.getContent())); - exportCql(library.getContent(), outputFile); - return library; - } - - private Measure stripMeasure(Measure measure) { - measure.setText(null); - measure.setRelatedArtifact(filterRelatedArtifacts(measure.getRelatedArtifact())); - measure.setExtension(filterExtensions(measure.getExtension())); - return measure; - } - - private PlanDefinition stripPlanDefinition(PlanDefinition planDefinition) { - planDefinition.setText(null); - planDefinition.setRelatedArtifact(filterRelatedArtifacts(planDefinition.getRelatedArtifact())); - planDefinition.setExtension(filterExtensions(planDefinition.getExtension())); - return planDefinition; - } - - private Questionnaire stripQuestionnaire(Questionnaire questionnaire) { - questionnaire.setText(null); - questionnaire.setExtension(filterExtensions(questionnaire.getExtension())); - return questionnaire; - } - - private DomainResource stripResource(DomainResource resource) { - resource.setText(null); - return resource; - } - - private void exportCql(Attachment content, File libraryOutputLocation) { - if (content.getData() == null) { - return; - } - - var base64 = content.getDataElement().getValueAsString(); - var cql = new String(java.util.Base64.getDecoder().decode(base64)); - var cqlFileName = libraryOutputLocation.getName(); - if (cqlFileName.toLowerCase().startsWith("library-")) { - cqlFileName = cqlFileName.substring(8); - } - - cqlFileName = cqlFileName.substring(0, cqlFileName.lastIndexOf('.')) + ".cql"; - var cqlFile = libraryOutputLocation.toPath().getParent().resolve(cqlFileName).toFile(); - - content.setUrl(libraryOutputLocation.toPath().relativize(cqlFile.toPath()).toString()); - content.setDataElement(null); - writeFile(cqlFile, cql); - } - - private void exportCql(List content, File libraryOutputFile) { - for (Attachment attachment : content) { - if (CQL_CONTENT_TYPE.equals(attachment.getContentType())) { - exportCql(attachment, libraryOutputFile); - } - } - } -} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripper.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripper.java new file mode 100644 index 000000000..9db8b6e31 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripper.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.tooling.operations.stripcontent; + +import java.io.File; + +// Intentionally package-private. This is a package-internal API for ContentStripper +interface ContentStripper { + void stripFile(File inputPath, File outputPath, ContentStripperOptions options); +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentDstu3.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperDstu3.java similarity index 75% rename from tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentDstu3.java rename to tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperDstu3.java index 3dd6258e3..3ce70259b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentDstu3.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperDstu3.java @@ -4,12 +4,14 @@ import org.hl7.fhir.dstu3.model.Resource; import ca.uhn.fhir.context.FhirContext; -class StripContentDstu3 extends BaseStripContent { +class ContentStripperDstu3 extends BaseContentStripper { @Override protected FhirContext context() { return FhirContext.forDstu3Cached(); } + // NOTE: These two methods appear to be identical, but they are not. It's the + // types of the input and output parameters that are different. @Override protected org.hl7.fhir.r5.model.Resource convertToR5(Resource resource) { return VersionConvertorFactory_30_50.convertResource(resource); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperOptions.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperOptions.java new file mode 100644 index 000000000..08ffcd559 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperOptions.java @@ -0,0 +1,62 @@ +package org.opencds.cqf.tooling.operations.stripcontent; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +// Intentionally package-private. This is a package-internal API for ContentStripper +class ContentStripperOptions { + static final String CQL_CONTENT_TYPE = "text/cql"; + static final String ELM_JSON_CONTENT_TYPE = "application/elm+json"; + static final String ELM_XML_CONTENT_TYPE = "application/elm+xml"; + + static final Set DEFAULT_STRIPPED_CONTENT_TYPES = new HashSet<>( + Arrays.asList(ELM_JSON_CONTENT_TYPE, ELM_XML_CONTENT_TYPE)); + + static final Set DEFAULT_STRIPPED_EXTENSION_URLS = new HashSet<>( + Arrays.asList("http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-parameter", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-dataRequirement", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-logicDefinition", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-softwaresystem", + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-directReferenceCode", + "http://hl7.org/fhir/StructureDefinition/cqf-cqlOptions")); + + private ContentStripperOptions() { + // Intentionally empty, forces use of the static factory + } + + public static ContentStripperOptions defaultOptions() { + return new ContentStripperOptions(); + } + + private File cqlExportDirectory; + public File cqlExportDirectory() { + return cqlExportDirectory; + } + public ContentStripperOptions cqlExportDirectory(File cqlExportDirectory) { + this.cqlExportDirectory = cqlExportDirectory; + return this; + } + + private Set strippedContentTypes = DEFAULT_STRIPPED_CONTENT_TYPES; + public Set strippedContentTypes() { + return this.strippedContentTypes; + } + + public ContentStripperOptions strippedContentTypes(Set strippedContentTypes) { + this.strippedContentTypes = strippedContentTypes; + return this; + } + + private Set strippedExtensionUrls = DEFAULT_STRIPPED_EXTENSION_URLS; + public Set strippedExtensionUrls() { + return this.strippedExtensionUrls; + } + + public ContentStripperOptions strippedExtensionUrls(Set strippedExtensionUrls) { + this.strippedExtensionUrls = strippedExtensionUrls; + return this; + } + +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentR4.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR4.java similarity index 75% rename from tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentR4.java rename to tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR4.java index a81ba838c..cc2b7e92b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentR4.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR4.java @@ -5,12 +5,14 @@ import ca.uhn.fhir.context.FhirContext; -class StripContentR4 extends BaseStripContent { +class ContentStripperR4 extends BaseContentStripper { @Override protected FhirContext context() { return FhirContext.forR4Cached(); } + // NOTE: These two methods appear to be identical, but they are not. It's the + // types of the input and output parameters that are different. @Override protected org.hl7.fhir.r5.model.Resource convertToR5(Resource resource) { return VersionConvertorFactory_40_50.convertResource(resource); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentR5.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR5.java similarity index 67% rename from tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentR5.java rename to tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR5.java index 13f5823fb..1c6bcdea6 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentR5.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR5.java @@ -4,11 +4,14 @@ import ca.uhn.fhir.context.FhirContext; -class StripContentR5 extends BaseStripContent { +class ContentStripperR5 extends BaseContentStripper { @Override protected FhirContext context() { return FhirContext.forR5Cached(); } + + // NOTE: These two methods appear to be identical, but they are not. It's the + // types of the input and output parameters that are different. @Override protected Resource convertToR5(Resource resource) { return resource; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/IStripContent.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/IStripContent.java deleted file mode 100644 index 48e640b54..000000000 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/IStripContent.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opencds.cqf.tooling.operations.stripcontent; - -import java.io.File; - -interface IStripContent { - void stripFile(File inputFile, File outputFile); -} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentExecutor.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentExecutor.java new file mode 100644 index 000000000..631596675 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentExecutor.java @@ -0,0 +1,82 @@ +package org.opencds.cqf.tooling.operations.stripcontent; + +import java.io.File; +import java.util.Collection; + +import org.apache.commons.io.FileUtils; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkArgument; +import ca.uhn.fhir.context.FhirVersionEnum; + +/** + * This class runs executes the StripContent command line operation. It picks the correct version + * of a ContentStripper based on the FHIR version specified in the command line arguments. It then + * iterates over all the files in the input directory and runs the stripFile method on each file. + */ +public class StripContentExecutor { + + private FhirVersionEnum versionEnum; + private File inputDirectory; + private File outputDirectory; + private String cqlExportDirectory; + + public StripContentExecutor(StripContentParams params) { + checkNotNull(params, "params must be provided"); + checkArgument(params.inputDirectory() != null, "inputDirectory must be provided"); + checkArgument(params.outputDirectory() != null, "outputDirectory must be provided"); + this.versionEnum = versionForString(params.fhirVersion()); + this.inputDirectory = validateDirectory(params.inputDirectory()); + this.outputDirectory = new File(params.outputDirectory()); + this.cqlExportDirectory = params.cqlExportDirectory(); + } + + public void execute() { + var files = listResourceFiles(inputDirectory); + var contentStripper = createContentStripper(); + var options = createContentStripperOptions(); + for (File file : files) { + // Keep the same filename, but change the directory to the output directory + var outputFile = outputDirectory.toPath().resolve(file.getName()).toFile(); + contentStripper.stripFile(file, outputFile, options); + } + } + + private ContentStripperOptions createContentStripperOptions() { + var cqlExportFile = this.cqlExportDirectory != null ? new File(this.cqlExportDirectory) : null; + return ContentStripperOptions.defaultOptions().cqlExportDirectory(cqlExportFile); + } + + private ContentStripper createContentStripper() { + switch (versionEnum) { + case DSTU3: + return new ContentStripperDstu3(); + case R4: + return new ContentStripperR4(); + case R5: + return new ContentStripperR5(); + default: + throw new IllegalArgumentException("Unsupported FHIR version"); + } + } + + private File validateDirectory(String pathToDir) { + checkNotNull(pathToDir, "The path to the directory is required"); + File directory = new File(pathToDir); + if (!directory.isDirectory()) { + throw new IllegalArgumentException("The path supplied is not a directory"); + } + return directory; + } + + private Collection listResourceFiles(File file) { + return FileUtils.listFiles(file, new String[] { "json", "xml"}, true); + } + + private FhirVersionEnum versionForString(String version) { + if (version == null) { + return FhirVersionEnum.R4; + } + return FhirVersionEnum.forVersionString(version.toUpperCase()); + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentParams.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentParams.java index e8d62dc50..572cf3862 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentParams.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentParams.java @@ -2,34 +2,44 @@ public class StripContentParams { - private String inputPath; - private String outputPath; - private String version; + private String inputDirectory; + private String outputDirectory; + private String fhirVersion; + private String cqlExportDirectory; - public String inputPath() { - return inputPath; + public String inputDirectory() { + return inputDirectory; } - public StripContentParams inputPath(String inputPath) { - this.inputPath = inputPath; + public StripContentParams inputDirectory(String inputDirectory) { + this.inputDirectory = inputDirectory; return this; } - public String outputPath() { - return outputPath; + public String outputDirectory() { + return outputDirectory; } - public StripContentParams outputPath(String outputPath) { - this.outputPath = outputPath; + public StripContentParams outputDirectory(String outputDirectory) { + this.outputDirectory = outputDirectory; return this; } - public String version() { - return version; + public String fhirVersion() { + return fhirVersion; } - public StripContentParams version(String version) { - this.version = version; + public StripContentParams version(String fhirVersion) { + this.fhirVersion = fhirVersion; + return this; + } + + public String cqlExportDirectory() { + return cqlExportDirectory; + } + + public StripContentParams cqlExportDirectory(String cqlExportDirectory) { + this.cqlExportDirectory = cqlExportDirectory; return this; } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentProcessor.java deleted file mode 100644 index 42245b010..000000000 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/StripContentProcessor.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.opencds.cqf.tooling.operations.stripcontent; - -import java.io.File; -import java.util.Collection; - -import org.apache.commons.io.FileUtils; - -import com.google.common.base.Preconditions; -import ca.uhn.fhir.context.FhirVersionEnum; -public class StripContentProcessor { - - private FhirVersionEnum versionEnum; - private File inputRoot; - private File outputRoot; - public StripContentProcessor(StripContentParams params) { - Preconditions.checkNotNull(params); - Preconditions.checkArgument(params.inputPath() != null, "Input path must be provided"); - Preconditions.checkArgument(params.outputPath() != null, "Output path must be provided"); - this.versionEnum = versionForString(params.version()); - this.inputRoot = validateDirectory(params.inputPath()); - this.outputRoot = new File(params.outputPath()); - } - - public void execute() { - var files = getListOfActionableFiles(inputRoot); - - IStripContent stripContent = null; - switch (versionEnum) { - case DSTU3: - stripContent = new StripContentDstu3(); - break; - case R4: - stripContent = new StripContentR4(); - break; - case R5: - stripContent = new StripContentR5(); - break; - default: - throw new IllegalArgumentException("Unsupported FHIR version"); - } - - for (File file : files) { - var outputFile = outputRoot.toPath().resolve(file.getName()).toFile(); - stripContent.stripFile(file, outputFile); - } - } - - private File validateDirectory(String pathToDir) { - if (pathToDir == null) { - throw new IllegalArgumentException("The path to the directory is required"); - } - - File bundleDirectory = new File(pathToDir); - if (!bundleDirectory.isDirectory()) { - throw new IllegalArgumentException("The path supplied is not a directory"); - } - return bundleDirectory; - } - - private Collection getListOfActionableFiles(File file) { - return FileUtils.listFiles(file, new String[] { "json", "xml"}, true); - } - - private FhirVersionEnum versionForString(String version) { - if (version == null) { - return FhirVersionEnum.R4; - } - return FhirVersionEnum.forVersionString(version.toUpperCase()); - } -} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperationTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperationTest.java index 6b15fa14d..8cb3f4079 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperationTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperationTest.java @@ -1,6 +1,7 @@ package org.opencds.cqf.tooling.operation; import ca.uhn.fhir.context.FhirContext; + import org.hl7.fhir.r4.model.Library; import org.opencds.cqf.tooling.Operation; import org.testng.annotations.Test; @@ -13,21 +14,18 @@ import java.io.FileReader; import java.net.URISyntaxException; import java.nio.file.Path; -import java.nio.file.Paths; public class StripGeneratedContentOperationTest { - private static final String separator = System.getProperty("file.separator"); @Test public void test_strip_generated_content() throws URISyntaxException, FileNotFoundException { String dataInputPath = "strip-resources"; String operation = "StripGeneratedContent"; - String inputFilePath = StripGeneratedContentOperationTest.class.getResource(dataInputPath).toURI().getPath(); - String outputPath = "target/test-output/strip-generated-content"; + var inputFilePath = Path.of(StripGeneratedContentOperationTest.class.getResource(dataInputPath).toURI()); + var outputPath = Path.of("target", "test-output", "strip-generated-content"); String version = "r4"; - Library libraryBeforeStrip = (Library)FhirContext.forR4Cached().newJsonParser().parseResource( - new FileReader(inputFilePath+"/LibraryBreastCancerScreeningFHIR.json")); + new FileReader(inputFilePath + "/LibraryBreastCancerScreeningFHIR.json")); assertEquals(libraryBeforeStrip.getContent().size(), 3); assertTrue(libraryBeforeStrip.hasText()); @@ -39,30 +37,44 @@ public void test_strip_generated_content() throws URISyntaxException, FileNotFou Operation stripGeneratedContentOperation = new StripGeneratedContentOperation(); stripGeneratedContentOperation.execute(args); - Library libraryAfterStrip = null; - if (separator.equalsIgnoreCase("/")) { + File jsonFile = outputPath.resolve("LibraryBreastCancerScreeningFHIR.json").toFile(); - Path path = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().getPath() + - "/../test-output/strip-generated-content"); - libraryAfterStrip = (Library)FhirContext.forR4Cached().newJsonParser().parseResource( - new FileReader(path + "/LibraryBreastCancerScreeningFHIR.json")); + var libraryAfterStrip = (Library) FhirContext.forR4Cached().newJsonParser().parseResource(new FileReader(jsonFile)); - }else{ + assertEquals(libraryAfterStrip.getContent().size(), 1); + // Cql should not be stripped or exported + assertTrue(libraryAfterStrip.getContent().get(0).hasData()); + assertFalse(libraryAfterStrip.hasText()); + assertFalse(libraryAfterStrip.hasParameter()); + assertFalse(libraryAfterStrip.hasDataRequirement()); + assertEquals(libraryAfterStrip.getRelatedArtifact().size(), 1); - File classLocation = new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); - File parentDir = classLocation.getParentFile(); // Get the parent directory of the class location - File outputDir = new File(parentDir, "test-output/"); - File jsonFile = new File(outputDir, "strip-generated-contentLibraryBreastCancerScreeningFHIR.json"); + } - libraryAfterStrip = (Library) FhirContext.forR4Cached().newJsonParser().parseResource(new FileReader(jsonFile)); - } + @Test + void exportsCql() throws URISyntaxException, FileNotFoundException { + String dataInputPath = "strip-resources"; + String operation = "StripGeneratedContent"; + var inputFilePath = Path.of(StripGeneratedContentOperationTest.class.getResource(dataInputPath).toURI()); + var outputPath = Path.of("target", "test-output", "strip-generated-content-cql"); + + String[] args = { "-" + operation, "-ptr=" + inputFilePath, "-op=" + outputPath, "-cql=" + outputPath + File.separator + "cql"}; + Operation stripGeneratedContentOperation = new StripGeneratedContentOperation(); + stripGeneratedContentOperation.execute(args); + File jsonFile = outputPath.resolve("LibraryBreastCancerScreeningFHIR.json").toFile(); + var libraryAfterStrip = (Library) FhirContext.forR4Cached().newJsonParser().parseResource(new FileReader(jsonFile)); assertEquals(libraryAfterStrip.getContent().size(), 1); + // Cql should be exported + assertFalse(libraryAfterStrip.getContent().get(0).hasData()); + assertTrue(libraryAfterStrip.getContent().get(0).hasUrl()); assertFalse(libraryAfterStrip.hasText()); assertFalse(libraryAfterStrip.hasParameter()); assertFalse(libraryAfterStrip.hasDataRequirement()); assertEquals(libraryAfterStrip.getRelatedArtifact().size(), 1); + File cqlFile = outputPath.resolve("cql").resolve("BreastCancerScreeningFHIR.cql").toFile(); + assertTrue(cqlFile.exists()); } }