From f922c53a81a7791b0e4381d1d8bf0acc415b108b Mon Sep 17 00:00:00 2001 From: leifstawnyczy Date: Tue, 11 Nov 2025 10:01:04 -0500 Subject: [PATCH 1/2] adding a test to read a resource out of an npm ig --- .../packages/NpmJpaValidationSupportIT.java | 95 +++++++++ hapi-fhir-test-utilities/pom.xml | 7 + .../implementationguide/GZipCreatorUtil.java | 58 +++++ .../ImplementationGuideCreator.java | 200 ++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java new file mode 100644 index 00000000000..18fa974dbbe --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupportIT.java @@ -0,0 +1,95 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.implementationguide.ImplementationGuideCreator; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.test.util.LogbackTestExtension; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NpmJpaValidationSupportIT extends BaseJpaR4Test { + @Autowired + private NpmJpaValidationSupport mySvc; + + @Autowired + private IPackageInstallerSvc myPackageInstallerSvc; + + @RegisterExtension + public final LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension(NpmJpaValidationSupport.class); + + @Test + public void loadIGAndFetchSpecificResource(@TempDir Path theTempDirPath) throws IOException { + // create an IG + ImplementationGuideCreator igCreator = new ImplementationGuideCreator(myFhirContext); + igCreator.setDirectory(theTempDirPath); + + // create some resources + String spUrl = "http://localhost/ig-test-dir/sp"; + String qUrl = "http://localhost/ig-test-dir/q"; + for (int i = 0; i < 2; i++) { + SearchParameter sp = new SearchParameter(); + sp.setUrl(spUrl + i); + sp.setCode("helloWorld" + i); + sp.setName(sp.getCode()); + sp.setDescription("description"); + sp.setType(Enumerations.SearchParamType.STRING); + sp.addBase("Patient"); + sp.setExpression("Patient.name.given"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + igCreator.addResourceToIG(sp.getName(), sp); + + Questionnaire questionnaire = new Questionnaire(); + questionnaire.setUrl(qUrl + i); + questionnaire.setStatus(Enumerations.PublicationStatus.ACTIVE); + questionnaire.setName("questionnaire" + i); + igCreator.addResourceToIG(questionnaire.getName(), questionnaire); + + // others? + } + + PackageInstallationSpec installationSpec = createAndInstallPackageSpec(igCreator, PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + + IBaseResource sp = mySvc.fetchResource("SearchParameter", spUrl + "1"); + assertNotNull(sp); + assertTrue(sp instanceof SearchParameter); + assertEquals(spUrl + "1", ((SearchParameter)sp).getUrl()); + IBaseResource q = mySvc.fetchResource("Questionnaire", qUrl + "1"); + assertNotNull(q); + assertTrue(q instanceof Questionnaire); + assertEquals(qUrl + "1", ((Questionnaire)q).getUrl()); + } + + private PackageInstallationSpec createAndInstallPackageSpec(ImplementationGuideCreator theIgCreator, + PackageInstallationSpec.InstallModeEnum theInstallModeEnum) throws IOException { + // create a source directory + Path outputFileName = theIgCreator.createTestIG(); + + // add some NPM package + PackageInstallationSpec spec = new PackageInstallationSpec() + .setName(theIgCreator.getPackageName()) + .setVersion(theIgCreator.getPackageVersion()) + .setInstallMode(theInstallModeEnum) + .setPackageContents(Files.readAllBytes(outputFileName)) + ; + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); + + assertNotNull(outcome); + assertTrue(outcome.getMessage() + .stream().anyMatch(m -> m.contains("Successfully added package")), + String.join(", ", outcome.getMessage())); + return spec; + } +} diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 76bcf64c13d..8e86c20a848 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -209,6 +209,13 @@ org.assertj assertj-core + + + + org.apache.commons + commons-compress + 1.28.0 + diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java new file mode 100644 index 00000000000..03ff8e32688 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java @@ -0,0 +1,58 @@ +package ca.uhn.fhir.implementationguide; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class GZipCreatorUtil { + + /** + * Create a tarball of the provided input and saves it to the provided output. + * + * @param theSource - the input path to the directory containing all source files to be zipped + * @param theOutput - the output path to the gzip file. + */ + public static void createTarGz(Path theSource, Path theOutput) throws IOException { + try (OutputStream fos = Files.newOutputStream(theOutput); + BufferedOutputStream bos = new BufferedOutputStream(fos); + GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); + TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) { + addFilesToTarGz(theSource, "", taos); + } + } + + private static void addFilesToTarGz(Path thePath, String theParent, TarArchiveOutputStream theTarballOutputStream) + throws IOException { + String entryName = theParent + thePath.getFileName().toString(); + TarArchiveEntry entry = new TarArchiveEntry(thePath.toFile(), entryName); + theTarballOutputStream.putArchiveEntry(entry); + + if (Files.isRegularFile(thePath)) { + // add file + try (InputStream fis = Files.newInputStream(thePath)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = fis.read(buffer)) > 0) { + theTarballOutputStream.write(buffer, 0, len); + } + } + theTarballOutputStream.closeArchiveEntry(); + } else { + theTarballOutputStream.closeArchiveEntry(); + // walk directory + try (DirectoryStream stream = Files.newDirectoryStream(thePath)) { + for (Path child : stream) { + addFilesToTarGz(child, entryName + "/", theTarballOutputStream); + } + } + } + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java new file mode 100644 index 00000000000..8d5a0721d3a --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java @@ -0,0 +1,200 @@ +package ca.uhn.fhir.implementationguide; + + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.util.FhirTerser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.intellij.lang.annotations.Language; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ImplementationGuideCreator { + + private static final Logger ourLog = LoggerFactory.getLogger(ImplementationGuideCreator.class); + + @Language("JSON") + private static final String PACKAGE_JSON_BASE = + """ + { + "name": "test.fhir.ca.com", + "version": "1.2.3", + "tools-version": 3, + "type": "fhir.ig", + "date": "20200831134427", + "license": "not-open-source", + "canonical": "http://test-ig.com/fhir/us/providerdataexchange", + "url": "file://C:\\\\dev\\\\test-exchange\\\\fsh\\\\build\\\\output", + "title": "Test Implementation Guide", + "description": "Test Implementation Guide", + "fhirVersions": [ + "4.0.1" + ], + "dependencies": { + }, + "author": "SmileCDR", + "maintainers": [ + { + "name": "Smile", + "email": "smilecdr@smiledigitalhealth.com", + "url": "https://www.smilecdr.com" + } + ], + "directories": { + "lib": "package", + "example": "example" + } + } +"""; + + private Path myDir; + private final FhirContext myFhirContext; + + private final FhirTerser myTerser; + private final IParser myParser; + + private final String myPackageJson; + + private final String myPackageName; + private final String myPackageVersion; + + private final Map myResourcesToInclude = new HashMap<>(); + + public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext) throws JsonProcessingException { + this(theFhirContext, "test.fhir.ca.com", "1.2.3"); + } + + public ImplementationGuideCreator( + @Nonnull FhirContext theFhirContext, String thePackageName, String thePackageVersion) + throws JsonProcessingException { + this( + theFhirContext, + theFhirContext.getVersion().getVersion().getFhirVersionString(), + thePackageName, + thePackageVersion); + } + + /** + * Constructor + * @param theFhirContext - FhirContext to use + * @param theFhirVersion - fhir version to use (provided to allow setting a custom value different form the FhirContext) + * @param theName - name to set in package.json's name field + * @param theVersion - version to set in package.json's version field + */ + @SuppressWarnings("unchecked") + public ImplementationGuideCreator( + @Nonnull FhirContext theFhirContext, String theFhirVersion, String theName, String theVersion) + throws JsonProcessingException { + myFhirContext = theFhirContext; + myTerser = myFhirContext.newTerser(); + myParser = myFhirContext.newJsonParser(); + myPackageName = theName; + myPackageVersion = theVersion; + + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + + Map mapJson = mapper.readValue(PACKAGE_JSON_BASE, Map.class); + + // update provided values + List versions = (List) mapJson.get("fhirVersions"); + versions.clear(); + versions.add(theFhirVersion); + mapJson.replace("name", myPackageName); + mapJson.replace("version", myPackageVersion); + + myPackageJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mapJson); + + ourLog.info(myPackageJson); + } + + /** + * Sets the directory where files will be created. + * Should be a temp dir for tests. + */ + public ImplementationGuideCreator setDirectory(Path thePath) { + myDir = thePath; + return this; + } + + public String getPackageName() { + return myPackageName; + } + + public String getPackageVersion() { + return myPackageVersion; + } + + /** + * Adds a Resource to the ImplementationGuide. + * No validation is done + */ + public void addResourceToIG(String theFileName, IBaseResource theResource) { + myResourcesToInclude.put(theFileName, theResource); + } + + private void verifyDir() { + String msg = "Directory must be set first."; + + assertNotNull(myDir, msg); + assertTrue(isNotBlank(myDir.toString()), msg); + } + + /** + * Creates an the IG from all the provided SearchParameters, + * zips them up, and provides the path to the newly created gzip file. + */ + public Path createTestIG() throws IOException { + verifyDir(); + + Path sourceDir = Files.createDirectory(Path.of(myDir.toString(), "package")); + + // add the package.json + addFileToDir(myPackageJson, "package.json", sourceDir); + + // add search parameters + int index = 0; + for (Map.Entry nameAndResource : myResourcesToInclude.entrySet()) { + addFileToDir(myParser.encodeResourceToString(nameAndResource.getValue()), nameAndResource.getKey() + ".json", sourceDir); + index++; + } + + // we can add other resources here (not req'd for now) + + Path outputFileName = Files.createFile(Path.of(myDir.toString(), myPackageName + ".gz.tar")); + GZipCreatorUtil.createTarGz(sourceDir, outputFileName); + return outputFileName; + } + + private void addFileToDir(String theContent, String theFileName, Path theOutputPath) throws IOException { + byte[] bytes = new byte[1024]; + int length = 0; + + try (FileOutputStream outputStream = new FileOutputStream(theOutputPath.toString() + "/" + theFileName)) { + try (InputStream stream = new ByteArrayInputStream(theContent.getBytes(StandardCharsets.UTF_8))) { + while ((length = stream.read(bytes)) >= 0) { + outputStream.write(bytes, 0, length); + } + } + } + } +} From d12350f8dcb16c75994efbbbce96954b687e574f Mon Sep 17 00:00:00 2001 From: leifstawnyczy Date: Tue, 11 Nov 2025 10:04:02 -0500 Subject: [PATCH 2/2] spotless --- .../implementationguide/GZipCreatorUtil.java | 8 +++---- .../ImplementationGuideCreator.java | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java index 03ff8e32688..fa7f677dac5 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/GZipCreatorUtil.java @@ -22,15 +22,15 @@ public class GZipCreatorUtil { */ public static void createTarGz(Path theSource, Path theOutput) throws IOException { try (OutputStream fos = Files.newOutputStream(theOutput); - BufferedOutputStream bos = new BufferedOutputStream(fos); - GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); - TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) { + BufferedOutputStream bos = new BufferedOutputStream(fos); + GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); + TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos)) { addFilesToTarGz(theSource, "", taos); } } private static void addFilesToTarGz(Path thePath, String theParent, TarArchiveOutputStream theTarballOutputStream) - throws IOException { + throws IOException { String entryName = theParent + thePath.getFileName().toString(); TarArchiveEntry entry = new TarArchiveEntry(thePath.toFile(), entryName); theTarballOutputStream.putArchiveEntry(entry); diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java index 8d5a0721d3a..8dc5c9a7441 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java @@ -1,6 +1,5 @@ package ca.uhn.fhir.implementationguide; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.util.FhirTerser; @@ -34,7 +33,7 @@ public class ImplementationGuideCreator { @Language("JSON") private static final String PACKAGE_JSON_BASE = - """ + """ { "name": "test.fhir.ca.com", "version": "1.2.3", @@ -84,13 +83,13 @@ public ImplementationGuideCreator(@Nonnull FhirContext theFhirContext) throws Js } public ImplementationGuideCreator( - @Nonnull FhirContext theFhirContext, String thePackageName, String thePackageVersion) - throws JsonProcessingException { + @Nonnull FhirContext theFhirContext, String thePackageName, String thePackageVersion) + throws JsonProcessingException { this( - theFhirContext, - theFhirContext.getVersion().getVersion().getFhirVersionString(), - thePackageName, - thePackageVersion); + theFhirContext, + theFhirContext.getVersion().getVersion().getFhirVersionString(), + thePackageName, + thePackageVersion); } /** @@ -102,8 +101,8 @@ public ImplementationGuideCreator( */ @SuppressWarnings("unchecked") public ImplementationGuideCreator( - @Nonnull FhirContext theFhirContext, String theFhirVersion, String theName, String theVersion) - throws JsonProcessingException { + @Nonnull FhirContext theFhirContext, String theFhirVersion, String theName, String theVersion) + throws JsonProcessingException { myFhirContext = theFhirContext; myTerser = myFhirContext.newTerser(); myParser = myFhirContext.newJsonParser(); @@ -174,7 +173,10 @@ public Path createTestIG() throws IOException { // add search parameters int index = 0; for (Map.Entry nameAndResource : myResourcesToInclude.entrySet()) { - addFileToDir(myParser.encodeResourceToString(nameAndResource.getValue()), nameAndResource.getKey() + ".json", sourceDir); + addFileToDir( + myParser.encodeResourceToString(nameAndResource.getValue()), + nameAndResource.getKey() + ".json", + sourceDir); index++; }