-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a CQL export option to the StripGeneratedContent operation (#511)
* Add CQL export to StripContent operation * More cleanup / refactoring of the strip content processor * Add suppression for errorneous warning * Cleanup and a test * Fix coment * Fix parameter name * Further clean-up * Swap to utility functions
- Loading branch information
Showing
12 changed files
with
479 additions
and
551 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
539 changes: 12 additions & 527 deletions
539
tooling/src/main/java/org/opencds/cqf/tooling/operation/StripGeneratedContentOperation.java
Large diffs are not rendered by default.
Oops, something went wrong.
201 changes: 201 additions & 0 deletions
201
...ng/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/BaseContentStripper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
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 static org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter.convertFromR5Resource; | ||
import static org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter.convertToR5Resource; | ||
|
||
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<T extends IAnyResource> implements ContentStripper { | ||
protected abstract FhirContext context(); | ||
|
||
public void stripFile(File inputFile, File outputFile, ContentStripperOptions options) { | ||
var resource = parseResource(inputFile); | ||
|
||
var upgraded = convertToR5Resource(context(), resource); | ||
stripResource(upgraded, outputFile, options); | ||
|
||
@SuppressWarnings("unchecked") | ||
var downgraded = (T) convertFromR5Resource(context(), 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 void filterContained(List<Resource> contained) { | ||
contained.removeIf(this::isCqlOptionsParameters); | ||
} | ||
|
||
private void filterExtensions(List<Extension> extensions, Set<String> strippedExtensions) { | ||
extensions.removeIf(x -> strippedExtensions.contains(x.getUrl())); | ||
} | ||
|
||
private void filterContent(List<Attachment> attachments, Set<String> strippedContentTypes) { | ||
attachments.removeIf(x -> strippedContentTypes.contains(x.getContentType())); | ||
} | ||
|
||
private void filterRelatedArtifacts(List<RelatedArtifact> artifacts) { | ||
artifacts.removeIf(x -> RelatedArtifact.RelatedArtifactType.DEPENDSON.equals(x.getType())); | ||
} | ||
|
||
// 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); | ||
filterRelatedArtifacts(library.getRelatedArtifact()); | ||
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); | ||
filterRelatedArtifacts(measure.getRelatedArtifact()); | ||
return measure; | ||
} | ||
|
||
private PlanDefinition stripPlanDefinition(PlanDefinition planDefinition, ContentStripperOptions options) { | ||
stripResource(planDefinition, options); | ||
filterRelatedArtifacts(planDefinition.getRelatedArtifact()); | ||
return planDefinition; | ||
} | ||
|
||
private Questionnaire stripQuestionnaire(Questionnaire questionnaire, ContentStripperOptions options) { | ||
stripResource(questionnaire, options); | ||
filterRelatedArtifacts(questionnaire.getRelatedArtifact()); | ||
return questionnaire; | ||
} | ||
|
||
private DomainResource stripResource(DomainResource resource, ContentStripperOptions options) { | ||
resource.setText(null); | ||
filterExtensions(resource.getExtension(), options.strippedExtensionUrls()); | ||
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<Attachment> content, String libraryName, File libraryOutputFile, File cqlExportDirectory) { | ||
for (Attachment attachment : content) { | ||
if (ContentStripperOptions.CQL_CONTENT_TYPE.equals(attachment.getContentType())) { | ||
exportCql(attachment, libraryName, libraryOutputFile, cqlExportDirectory); | ||
} | ||
} | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
11 changes: 11 additions & 0 deletions
11
...g/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperDstu3.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package org.opencds.cqf.tooling.operations.stripcontent; | ||
|
||
import org.hl7.fhir.dstu3.model.Resource; | ||
import ca.uhn.fhir.context.FhirContext; | ||
|
||
class ContentStripperDstu3 extends BaseContentStripper<Resource> { | ||
@Override | ||
protected FhirContext context() { | ||
return FhirContext.forDstu3Cached(); | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
...src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperOptions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> DEFAULT_STRIPPED_CONTENT_TYPES = new HashSet<>( | ||
Arrays.asList(ELM_JSON_CONTENT_TYPE, ELM_XML_CONTENT_TYPE)); | ||
|
||
static final Set<String> 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<String> strippedContentTypes = DEFAULT_STRIPPED_CONTENT_TYPES; | ||
public Set<String> strippedContentTypes() { | ||
return this.strippedContentTypes; | ||
} | ||
|
||
public ContentStripperOptions strippedContentTypes(Set<String> strippedContentTypes) { | ||
this.strippedContentTypes = strippedContentTypes; | ||
return this; | ||
} | ||
|
||
private Set<String> strippedExtensionUrls = DEFAULT_STRIPPED_EXTENSION_URLS; | ||
public Set<String> strippedExtensionUrls() { | ||
return this.strippedExtensionUrls; | ||
} | ||
|
||
public ContentStripperOptions strippedExtensionUrls(Set<String> strippedExtensionUrls) { | ||
this.strippedExtensionUrls = strippedExtensionUrls; | ||
return this; | ||
} | ||
|
||
} |
11 changes: 11 additions & 0 deletions
11
tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR4.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package org.opencds.cqf.tooling.operations.stripcontent; | ||
|
||
import org.hl7.fhir.r4.model.Resource; | ||
import ca.uhn.fhir.context.FhirContext; | ||
|
||
class ContentStripperR4 extends BaseContentStripper<Resource> { | ||
@Override | ||
protected FhirContext context() { | ||
return FhirContext.forR4Cached(); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
tooling/src/main/java/org/opencds/cqf/tooling/operations/stripcontent/ContentStripperR5.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.opencds.cqf.tooling.operations.stripcontent; | ||
|
||
import org.hl7.fhir.r5.model.Resource; | ||
|
||
import ca.uhn.fhir.context.FhirContext; | ||
|
||
class ContentStripperR5 extends BaseContentStripper<Resource> { | ||
@Override | ||
protected FhirContext context() { | ||
return FhirContext.forR5Cached(); | ||
} | ||
} |
Oops, something went wrong.