Skip to content

Commit ed172f4

Browse files
echicoine-icfEvan Chicoinebrynrhodesc-schuler
authored
BUG FIX (Issue 537): ExtractMatBundle Non-IG Folder Structure Handling (#539)
* Beefed up directory checks at extraction for instances where ig folder structure is not present. * Traversing the folder structure to establish bundles folder * File.separator used instead of hardcoded forward slashes in paths * Extraction process will use location of bundle file as location for extraction (output) after ig folder structure check fails to provide directory. Test updated to verify bundles folder no longer needed for extraction. * Cleaning up * Added comments for clarity * Attempting to correct test. * Fixed test using BeforeMethod instead of BeforeClass to create new instance of class. * Erroneous use of File.separator in encoding identifier --------- Co-authored-by: Evan Chicoine <evan.chicoine@esacinc.com> Co-authored-by: Bryn Rhodes <brynrhodes@users.noreply.github.com> Co-authored-by: c-schuler <hoofschu@gmail.com>
1 parent 33045fa commit ed172f4

13 files changed

Lines changed: 1068 additions & 32 deletions

tooling/src/main/java/org/opencds/cqf/tooling/operation/ExtractMatBundleOperation.java

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import org.opencds.cqf.tooling.Operation;
77
import org.opencds.cqf.tooling.common.ThreadUtils;
88
import org.opencds.cqf.tooling.utilities.BundleUtils;
9-
import org.opencds.cqf.tooling.utilities.LogUtils;
109
import org.opencds.cqf.tooling.utilities.ResourceUtils;
1110
import org.slf4j.Logger;
1211
import org.slf4j.LoggerFactory;
@@ -19,9 +18,9 @@
1918
import java.util.ArrayList;
2019
import java.util.Base64;
2120
import java.util.List;
21+
import java.util.Objects;
2222
import java.util.concurrent.Callable;
2323
import java.util.concurrent.CopyOnWriteArrayList;
24-
import java.util.stream.Collectors;
2524

2625
public class ExtractMatBundleOperation extends Operation {
2726
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@@ -60,7 +59,9 @@ public void execute(String[] args) {
6059
} else if (i == 0 && !args[i].equalsIgnoreCase("-ExtractMatBundle")) {
6160
throw new IllegalArgumentException("Insufficient argument structure. " +
6261
"Usage Example: mvn exec:java -Dexec.args=\"-ExtractMatBundle " +
63-
"/Development/ecqm-content-r4/bundles/mat/EXM124/EXM124.json -v=r4");
62+
File.separator + "Development" + File.separator + "ecqm-content-r4" +
63+
File.separator + "bundles" + File.separator + "mat" +
64+
File.separator + "EXM124" + File.separator + "EXM124.json -v=r4");
6465
}
6566

6667
//position 1 is the location of file or directory. Determine which:
@@ -135,7 +136,7 @@ public void execute(String[] args) {
135136
if (filesInDir != null && filesInDir.length > 0) {
136137
//use recursive calls to build up task list:
137138
ThreadUtils.executeTasks(processFilesInDir(filesInDir, version, suppressNarrative));
138-
}else{
139+
} else {
139140
logger.info(ERROR_DIR_IS_EMPTY);
140141
return;
141142
}
@@ -146,7 +147,7 @@ public void execute(String[] args) {
146147

147148
if (!processedBundleCollection.isEmpty()) {
148149
logger.info("Successfully extracted " + processedBundleCollection.size() + " resource(s): \n" + String.join("\n", processedBundleCollection));
149-
}else{
150+
} else {
150151
logger.info("ExtractMatBundleOperation ended with no resources extracted!");
151152
}
152153
}
@@ -268,6 +269,22 @@ void processSingleFile(File bundleFile, String version, boolean suppressNarrativ
268269
}
269270

270271

272+
private Path getParentBundleDir(String directory){
273+
Path parent = Paths.get(directory);
274+
// Traverse to the parent of 'bundles'
275+
while (parent != null &&
276+
parent.getFileName() != null &&
277+
!parent.getFileName().toString().equalsIgnoreCase("bundles")) {
278+
parent = parent.getParent();
279+
}
280+
281+
// Move one level up to get the directory before 'bundles'
282+
if (parent != null) {
283+
return parent.getParent();
284+
} else {
285+
return null;
286+
}
287+
}
271288

272289
/**
273290
* Iterates through the files and properly renames and moves them to the proper place
@@ -298,14 +315,21 @@ private void moveAndRenameFiles(String outputDir, FhirContext context, String ve
298315
// The extractor code names them using the resource type and ID
299316
// We want to name them without the resource type, use name, and if needed version
300317
String resourceName;
301-
Path newOutputDirectory = Paths.get(outputDir.substring(0, outputDir.indexOf("bundles")), "input");
302-
Path newLibraryDirectory = Paths.get(newOutputDirectory.toString(), "resources/library");
303-
newLibraryDirectory.toFile().mkdirs();
318+
319+
// https://github.com/cqframework/cqf-tooling/issues/537
320+
// if there's no bundle directory (getParentBundleDir returned null) we'll use resourceOutputDir (original outputDir):
321+
Path resourceOutputDir = Paths.get(outputDir).toAbsolutePath();
322+
323+
Path newOutputDirectory = Paths.get(
324+
Objects.requireNonNullElse(getParentBundleDir(outputDir)
325+
, resourceOutputDir)
326+
+ File.separator + "input");
327+
328+
Path newLibraryDirectory = Paths.get(newOutputDirectory.toString(), "resources" + File.separator + "library");
304329
Path newCqlDirectory = Paths.get(newOutputDirectory.toString(), "cql");
305-
newCqlDirectory.toFile().mkdirs();
306-
Path newMeasureDirectory = Paths.get(newOutputDirectory.toString(), "resources/measure");
307-
newMeasureDirectory.toFile().mkdirs();
308-
if (version == "stu3) ") {
330+
Path newMeasureDirectory = Paths.get(newOutputDirectory.toString(), "resources" + File.separator + "measure");
331+
332+
if (version.equals(VERSION_STU3)) {
309333
if (theResource instanceof org.hl7.fhir.dstu3.model.Library) {
310334
org.hl7.fhir.dstu3.model.Library theLibrary = (org.hl7.fhir.dstu3.model.Library) theResource;
311335
resourceName = theLibrary.getName();
@@ -329,7 +353,7 @@ private void moveAndRenameFiles(String outputDir, FhirContext context, String ve
329353
// Forcing the encoding to JSON here to make everything the same in input directory
330354
ResourceUtils.outputResourceByName(theResource, "json", context, newMeasureDirectory.toString(), resourceName);
331355
}
332-
} else if (version == VERSION_R4) {
356+
} else if (version.equals(VERSION_R4)) {
333357
if (theResource instanceof org.hl7.fhir.r4.model.Library) {
334358
org.hl7.fhir.r4.model.Library theLibrary = (org.hl7.fhir.r4.model.Library) theResource;
335359
resourceName = theLibrary.getName();
@@ -372,7 +396,13 @@ private void extractStu3CQL(org.hl7.fhir.dstu3.model.Library theLibrary, String
372396
String encodedString = Base64.getEncoder().encodeToString(encodedBytes);
373397
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
374398
try {
375-
FileUtils.writeByteArrayToFile(new File(cqlFilename), decodedBytes);
399+
File outputFile = new File(cqlFilename);
400+
// Ensure the parent directory exists
401+
File parentDir = outputFile.getParentFile();
402+
if (parentDir != null && !parentDir.exists()) {
403+
parentDir.mkdirs();
404+
}
405+
FileUtils.writeByteArrayToFile(outputFile, decodedBytes);
376406
} catch (IOException e) {
377407
throw new RuntimeException(cqlFilename + ": " + e.getMessage());
378408
}
@@ -395,7 +425,13 @@ private void extractR4CQL(org.hl7.fhir.r4.model.Library theLibrary, String cqlFi
395425
String encodedString = Base64.getEncoder().encodeToString(encodedBytes);
396426
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
397427
try {
398-
FileUtils.writeByteArrayToFile(new File(cqlFilename), decodedBytes);
428+
File outputFile = new File(cqlFilename);
429+
// Ensure the parent directory exists
430+
File parentDir = outputFile.getParentFile();
431+
if (parentDir != null && !parentDir.exists()) {
432+
parentDir.mkdirs();
433+
}
434+
FileUtils.writeByteArrayToFile(outputFile, decodedBytes);
399435
} catch (IOException e) {
400436
throw new RuntimeException(cqlFilename + ": " + e.getMessage());
401437
}

tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -838,13 +838,20 @@ public static void outputResource(IBaseResource resource, String encoding, FhirC
838838
}
839839

840840
public static void outputResourceByName(IBaseResource resource, String encoding, FhirContext context, String outputPath, String name) {
841-
try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + name + "." + encoding)) {
842-
writer.write(
843-
encoding.equals("json")
844-
? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes()
845-
: context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes()
846-
);
847-
writer.flush();
841+
try {
842+
File directory = new File(outputPath);
843+
if (!directory.exists()) {
844+
directory.mkdirs(); // Ensure the directory exists
845+
}
846+
847+
try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + name + "." + encoding)) {
848+
writer.write(
849+
encoding.equals("json")
850+
? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes()
851+
: context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes()
852+
);
853+
writer.flush();
854+
}
848855
} catch (IOException e) {
849856
e.printStackTrace();
850857
throw new RuntimeException(e.getMessage());

tooling/src/test/java/org/opencds/cqf/tooling/operation/ExtractMatBundleOperationTest.java

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
package org.opencds.cqf.tooling.operation;
22

3-
import static org.testng.Assert.assertEquals;
4-
import static org.testng.Assert.assertNotNull;
5-
import static org.testng.Assert.assertNull;
6-
import static org.testng.Assert.fail;
3+
import org.testng.annotations.BeforeMethod;
4+
import org.testng.annotations.Test;
75

86
import java.io.File;
97
import java.io.IOException;
108
import java.net.URL;
119
import java.nio.file.Files;
1210

13-
import org.testng.annotations.BeforeClass;
14-
import org.testng.annotations.Test;
11+
import static org.testng.Assert.*;
1512

1613
public class ExtractMatBundleOperationTest {
1714

1815
private ExtractMatBundleOperation operation;
1916

20-
@BeforeClass
17+
@BeforeMethod
2118
public void setUp() {
2219
operation = new ExtractMatBundleOperation();
2320
}
@@ -211,9 +208,61 @@ public void run() {
211208
}
212209
File[] files = emptyDir.listFiles();
213210
assertNotNull(files);
214-
assertEquals(16, files.length);
211+
assertEquals(17, files.length);
212+
}
213+
214+
/**
215+
* In response to issue https://github.com/cqframework/cqf-tooling/issues/537
216+
* The ExtractMATBundle process defaults to the location of the bundle file
217+
* when no IG folder structure exists (no bundle folder found.)
218+
*
219+
* @throws IOException
220+
*/
221+
@Test
222+
public void TestExtractMatBundleWithNonIGStructuredDirectory() throws IOException {
223+
ClassLoader classLoader = getClass().getClassLoader();
224+
String resourcePath = "org/opencds/cqf/tooling/operation/ExtractMatBundle/bundles_small/";
225+
URL resourceUrl = classLoader.getResource(resourcePath);
226+
if (resourceUrl == null) {
227+
throw new IllegalArgumentException("Resource not found: " + resourcePath);
228+
}
229+
230+
// Create a temporary directory named "noIG" and does not include the name bundles
231+
File tempDir = Files.createTempDirectory("noIG").toFile();
232+
tempDir.deleteOnExit();
233+
234+
// Copy files from resourcePath to the temporary directory
235+
File sourceDir = new File(resourceUrl.getFile());
236+
if (!sourceDir.isDirectory()) {
237+
throw new IllegalArgumentException("Resource path is not a directory: " + resourcePath);
238+
}
239+
240+
//Copy our test files to the temp folder so the source location of the bundle is the temp folder
241+
for (File file : sourceDir.listFiles()) {
242+
File destFile = new File(tempDir, file.getName());
243+
Files.copy(file.toPath(), destFile.toPath());
244+
}
245+
246+
Thread executionThread = new Thread(() ->
247+
operation.execute(new String[]{"-ExtractMATBundle", tempDir.getAbsolutePath(), "-dir"})
248+
);
249+
250+
executionThread.start();
251+
try {
252+
executionThread.join();
253+
} catch (InterruptedException e) {
254+
throw new RuntimeException(e);
255+
}
256+
257+
// Validate results in the temporary directory
258+
File[] files = tempDir.listFiles();
259+
assertNotNull(files);
260+
261+
//Directory should now include 1 input folder, original json bundles (6), and extracted files (16)
262+
assertEquals(23, files.length);
215263
}
216264

265+
217266
@Test
218267
public void TestExtractMatBundleWithDirectoryAndSubDirectories() throws IOException {
219268
ClassLoader classLoader = getClass().getClassLoader();
@@ -240,7 +289,7 @@ public void run() {
240289
}
241290
File[] files = emptyDir.listFiles();
242291
assertNotNull(files);
243-
assertEquals(41, files.length);
292+
assertEquals(42, files.length);
244293
}
245294

246295
@Test
@@ -265,6 +314,6 @@ public void run() {
265314

266315
File[] files = emptyDir.listFiles();
267316
assertNotNull(files);
268-
assertEquals(files.length, 16);
317+
assertEquals(files.length, 17);
269318
}
270319
}

tooling/src/test/resources/org/opencds/cqf/tooling/utilities/ecqm-content-r4-2021/bundles/Library-FHIRHelpers.json

Lines changed: 35 additions & 0 deletions
Large diffs are not rendered by default.

tooling/src/test/resources/org/opencds/cqf/tooling/utilities/ecqm-content-r4-2021/bundles/Library-HospitalHarmSevereHypoglycemiaFHIR.json

Lines changed: 92 additions & 0 deletions
Large diffs are not rendered by default.

tooling/src/test/resources/org/opencds/cqf/tooling/utilities/ecqm-content-r4-2021/bundles/Library-MATGlobalCommonFunctionsFHIR4.json

Lines changed: 105 additions & 0 deletions
Large diffs are not rendered by default.

tooling/src/test/resources/org/opencds/cqf/tooling/utilities/ecqm-content-r4-2021/bundles/Library-SupplementalDataElementsFHIR4.json

Lines changed: 58 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)