Skip to content

Commit 6d6da43

Browse files
Feature/convert r5 to r4 (#589)
* adds convert r5 to r4 operation * adds r5 to r4 operation tests * change default bundle type to transaction * adds ConvertR5toR4 command to operation factory * updates based on PR feedback * Improve ConvertR5toR4 CLI conversion tests and modernize implementation - Keeps the runtime behavior of ConvertR5toR4 intact. - Significantly strengthens the test suite by asserting that: - All CLI conversions produce R4 Bundles. - Entry counts match the number of source resources for both single and multiple inputs. - Error paths for invalid encoding and invalid directories are enforced. - Cleans up test code style and ensures no test artifacts persist on disk after execution. - Removed operations/convert/ConvertR5toR4 class --------- Co-authored-by: c-schuler <hoofschu@gmail.com>
1 parent 9b02acb commit 6d6da43

File tree

12 files changed

+424
-0
lines changed

12 files changed

+424
-0
lines changed

tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/OperationFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ static Operation createOperation(String operationName) {
223223
return new StripGeneratedContentOperation();
224224
case "SpreadsheetValidateVSandCS":
225225
return new SpreadsheetValidateVSandCS();
226+
case "ConvertR5toR4":
227+
return new ConvertR5toR4();
226228
default:
227229
throw new IllegalArgumentException("Invalid operation: " + operationName);
228230
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package org.opencds.cqf.tooling.operation;
2+
3+
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
4+
import ca.uhn.fhir.util.BundleBuilder;
5+
import jakarta.annotation.Nonnull;
6+
import org.hl7.fhir.instance.model.api.IBaseBundle;
7+
import org.hl7.fhir.instance.model.api.IBaseResource;
8+
import org.opencds.cqf.tooling.Operation;
9+
import org.opencds.cqf.tooling.utilities.BundleUtils;
10+
import org.opencds.cqf.tooling.utilities.FhirContextCache;
11+
import org.opencds.cqf.tooling.utilities.IOUtils;
12+
import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
import java.io.File;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.UUID;
20+
import java.util.stream.Collectors;
21+
22+
public class ConvertR5toR4 extends Operation {
23+
24+
public static final List<BundleTypeEnum> ALLOWED_BUNDLE_TYPES = List.of(
25+
BundleTypeEnum.COLLECTION,
26+
BundleTypeEnum.TRANSACTION
27+
);
28+
29+
public static Boolean isBundleTypeAllowed(String bundleType) {
30+
if (bundleType == null) {
31+
return false;
32+
}
33+
return ALLOWED_BUNDLE_TYPES.stream()
34+
.anyMatch(bt -> bt.name().equalsIgnoreCase(bundleType));
35+
}
36+
37+
public static List<String> allowedBundleTypes() {
38+
return ALLOWED_BUNDLE_TYPES.stream()
39+
.map(Enum::name)
40+
.map(String::toLowerCase)
41+
.collect(Collectors.toList());
42+
}
43+
44+
private static final Logger logger = LoggerFactory.getLogger(ConvertR5toR4.class);
45+
// COMMAND LINE ARGUMENTS - REQUIRED
46+
private String pathToDirectory; // -pathtodir (-ptd)
47+
48+
// COMMAND LINE ARGUMENTS - OPTIONAL
49+
private String encoding = "json"; // -encoding (-e)
50+
private String bundleId = UUID.randomUUID().toString(); // -bundleid (-bid)
51+
private String bundleType = "transaction"; // -type (-t)
52+
private String outputFileName = null; // -outputfilename (-ofn)
53+
54+
private void extractOptionsFromArgs(String[] args) {
55+
for (String arg : args) {
56+
if (arg.equals("-ConvertR5toR4")) continue;
57+
var flagAndValue = arg.split("=");
58+
if (flagAndValue.length < 2) {
59+
throw new IllegalArgumentException("Invalid argument: " + arg);
60+
}
61+
var flag = flagAndValue[0];
62+
var value = flagAndValue[1];
63+
64+
switch (flag.replace("-", "").toLowerCase()) {
65+
case "bundleid":
66+
case "bid":
67+
bundleId = value;
68+
break;
69+
case "bundletype":
70+
case "bt":
71+
bundleType = value;
72+
break;
73+
case "encoding":
74+
case "e":
75+
encoding = value.toLowerCase();
76+
break;
77+
case "outputfilename":
78+
case "ofn":
79+
outputFileName = value;
80+
break;
81+
case "outputpath":
82+
case "op":
83+
setOutputPath(value);
84+
break;
85+
case "pathtodir":
86+
case "ptd":
87+
pathToDirectory = value;
88+
break;
89+
default: throw new IllegalArgumentException("Unknown flag: " + flag);
90+
}
91+
}
92+
}
93+
94+
private void validateBundleType() {
95+
if (bundleType == null) {
96+
throw new IllegalArgumentException("BundleType cannot be null");
97+
}
98+
99+
if (!isBundleTypeAllowed(bundleType)) {
100+
throw new IllegalArgumentException(String.format("The bundle type [%s] is invalid. Allowed Types: %s", bundleType, String.join(", ",allowedBundleTypes())));
101+
}
102+
}
103+
104+
private void validateEncoding() {
105+
if (encoding == null || encoding.isEmpty()) {
106+
encoding = "json";
107+
} else {
108+
if (!encoding.equalsIgnoreCase("xml") && !encoding.equalsIgnoreCase("json")) {
109+
throw new IllegalArgumentException(String.format("Unsupported encoding: %s. Allowed encodings { json, xml }", encoding));
110+
}
111+
}
112+
}
113+
114+
private void validatePathToDirectory() {
115+
if (pathToDirectory == null) {
116+
throw new IllegalArgumentException(String.format("The path [%s] to the resource directory is required", pathToDirectory));
117+
}
118+
119+
var resourceDirectory = new File(pathToDirectory);
120+
if (!resourceDirectory.isDirectory()) {
121+
throw new RuntimeException(String.format("The specified path [%s] to resource files is not a directory", pathToDirectory));
122+
}
123+
124+
var resources = resourceDirectory.listFiles();
125+
if (resources == null || resources.length == 0) {
126+
throw new RuntimeException(String.format("The specified path [%s] to resource files is empty", pathToDirectory));
127+
}
128+
}
129+
130+
@Override
131+
public void execute(String[] args) {
132+
setOutputPath("src/main/resources/org/opencds/cqf/tooling/convert/output"); // default
133+
134+
extractOptionsFromArgs(args);
135+
validateEncoding();
136+
validatePathToDirectory();
137+
validateBundleType();
138+
139+
var bundleType = BundleUtils.getBundleType(this.bundleType);
140+
if (bundleType == null) {
141+
logger.error("Invalid bundle type: {}", this.bundleType);
142+
}
143+
else {
144+
var bundle = convertResources(
145+
bundleId,
146+
bundleType,
147+
IOUtils.readResources(
148+
IOUtils.getFilePaths(pathToDirectory, true),
149+
FhirContextCache.getContext("r5")));
150+
151+
IOUtils.writeResource(
152+
bundle,
153+
getOutputPath(),
154+
IOUtils.Encoding.parse(encoding),
155+
FhirContextCache.getContext("r4"),
156+
true,
157+
outputFileName != null ? outputFileName : bundleId);
158+
}
159+
}
160+
161+
private IBaseBundle convertResources(String bundleId, BundleTypeEnum type,
162+
@Nonnull List<IBaseResource> resourcesToConvert) {
163+
var convertedResources = new ArrayList<org.hl7.fhir.r4.model.Resource>();
164+
for (var resource : resourcesToConvert){
165+
if (resource instanceof org.hl7.fhir.r5.model.Resource) {
166+
convertedResources.add(ResourceAndTypeConverter.r5ToR4Resource(resource));
167+
}
168+
}
169+
170+
var context = FhirContextCache.getContext("r4");
171+
var builder = new BundleBuilder(context);
172+
if (type == BundleTypeEnum.COLLECTION) {
173+
convertedResources.forEach(builder::addCollectionEntry);
174+
}
175+
else {
176+
convertedResources.forEach(builder::addTransactionUpdateEntry);
177+
}
178+
var bundle = builder.getBundle();
179+
bundle.setId(bundleId == null ? UUID.randomUUID().toString() : bundleId);
180+
return bundle;
181+
}
182+
}

0 commit comments

Comments
 (0)