-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
433 additions
and
332 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
207 changes: 207 additions & 0 deletions
207
...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,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<T extends IAnyResource> 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<Resource> filterContained(List<Resource> contained) { | ||
return contained.stream() | ||
.filter(this::isCqlOptionsParameters) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private List<Extension> filterExtensions(List<Extension> extensions, Set<String> strippedExtensions) { | ||
return extensions.stream() | ||
.filter(extension -> !strippedExtensions.contains(extension.getUrl())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private List<Attachment> filterContent(List<Attachment> attachments, Set<String> strippedContentTypes) { | ||
return attachments.stream() | ||
.filter(attachment -> !strippedContentTypes.contains(attachment.getContentType())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private List<RelatedArtifact> filterRelatedArtifacts(List<RelatedArtifact> 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<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); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.