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..fa7f677dac5
--- /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..8dc5c9a7441
--- /dev/null
+++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/implementationguide/ImplementationGuideCreator.java
@@ -0,0 +1,202 @@
+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);
+ }
+ }
+ }
+ }
+}