Skip to content

Commit

Permalink
Cleanup and a test
Browse files Browse the repository at this point in the history
  • Loading branch information
JPercival committed Jan 25, 2024
1 parent 88686b9 commit 83f8ead
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 332 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
}
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);
}
}
}
}
Loading

0 comments on commit 83f8ead

Please sign in to comment.