diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java index 92d0600a42d..ba22f16b6ed 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java @@ -142,21 +142,6 @@ public enum FhirVersionEnum { throw new IllegalStateException("Unknown version: " + this); // should not happen } - /** - * Returns the {@link FhirVersionEnum} which corresponds to a specific version of - * FHIR. Partial version strings (e.g. "3.0") are acceptable. - * - * @return Returns null if no version exists matching the given string - */ - public static FhirVersionEnum forVersionString(String theVersionString) { - for (FhirVersionEnum next : values()) { - if (next.getFhirVersionString().startsWith(theVersionString)) { - return next; - } - } - return null; - } - private interface IVersionProvider { String provideVersion(); } @@ -241,4 +226,44 @@ public enum FhirVersionEnum { } + /** + * Returns the {@link FhirVersionEnum} which corresponds to a specific version of + * FHIR. Partial version strings (e.g. "3.0") are acceptable. This method will + * also accept version names such as "DSTU2", "STU3", "R5", etc. + * + * @return Returns null if no version exists matching the given string + */ + public static FhirVersionEnum forVersionString(String theVersionString) { + + // Trim the point release + String versionString = theVersionString; + int firstDot = versionString.indexOf('.'); + if (firstDot > 0) { + int secondDot = versionString.indexOf('.', firstDot + 1); + if (secondDot > 0) { + versionString = versionString.substring(0, secondDot); + } + } + + for (FhirVersionEnum next : values()) { + if (next.getFhirVersionString().startsWith(versionString)) { + return next; + } + } + + switch (theVersionString) { + case "DSTU2": + return FhirVersionEnum.DSTU2; + case "DSTU3": + case "STU3": + return FhirVersionEnum.DSTU3; + case "R4": + return FhirVersionEnum.R4; + case "R5": + return FhirVersionEnum.R5; + } + + return null; + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java index 7d1d3f2d135..f70fefca646 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java @@ -20,6 +20,9 @@ package ca.uhn.fhir.context.support; * #L% */ +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + public class ConceptValidationOptions { public boolean isInferSystem() { @@ -33,4 +36,10 @@ public class ConceptValidationOptions { private boolean myInferSystem; + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("inferSystem", myInferSystem) + .toString(); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/ExampleSupplier.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/ExampleSupplier.java new file mode 100644 index 00000000000..e546f1cf68d --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/ExampleSupplier.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.model.api.annotation; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Supplier; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface ExampleSupplier { + + Class>[] value(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index b8d66cac6ce..84d04d962e3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -270,6 +270,7 @@ public class Constants { * key will be of type {@link ca.uhn.fhir.interceptor.model.RequestPartitionId}. */ public static final String RESOURCE_PARTITION_ID = Constants.class.getName() + "_RESOURCE_PARTITION_ID"; + public static final String CT_APPLICATION_GZIP = "application/gzip"; static { CHARSET_UTF8 = StandardCharsets.UTF_8; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java index 20a2b9956c2..44299cac30d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BinaryUtil.java @@ -20,8 +20,15 @@ package ca.uhn.fhir.util; * #L% */ -import ca.uhn.fhir.context.*; -import org.hl7.fhir.instance.model.api.*; +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.util.List; @@ -84,4 +91,22 @@ public class BinaryUtil { reference.setReference(theSecurityContext); } + public static void setData(FhirContext theCtx, IBaseBinary theBinary, byte[] theBytes, String theContentType) { + getOrCreateData(theCtx, theBinary).setValue(theBytes); + + String elementName = "contentType"; + BaseRuntimeChildDefinition entryChild = AttachmentUtil.getChild(theCtx, theBinary, elementName); + List entries = entryChild.getAccessor().getValues(theBinary); + IPrimitiveType contentTypeElement = entries + .stream() + .map(t -> (IPrimitiveType) t) + .findFirst() + .orElseGet(() -> { + IPrimitiveType stringType = AttachmentUtil.newPrimitive(theCtx, "code", null); + entryChild.getMutator().setValue(theBinary, stringType); + return stringType; + }); + contentTypeElement.setValue(theContentType); + + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java new file mode 100644 index 00000000000..bbce1a65556 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.util; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class ResourceUtil { + + /** + * This method removes the narrative from the resource, or if the resource is a bundle, removes the narrative from + * all of the resources in the bundle + * + * @param theContext The fhir context + * @param theInput The resource to remove the narrative from + */ + public static void removeNarrative(FhirContext theContext, IBaseResource theInput) { + if (theInput instanceof IBaseBundle) { + for (IBaseResource next : BundleUtil.toListOfResources(theContext, (IBaseBundle) theInput)) { + removeNarrative(theContext, next); + } + } + + BaseRuntimeElementCompositeDefinition element = theContext.getResourceDefinition(theInput.getClass()); + BaseRuntimeChildDefinition textElement = element.getChildByName("text"); + if (textElement != null) { + textElement.getMutator().setValue(theInput, null); + } + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringNormalizer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java similarity index 78% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringNormalizer.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java index 02b3e7fd4c8..ca928b91d5e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringNormalizer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StringUtil.java @@ -21,10 +21,17 @@ package ca.uhn.fhir.util; */ import java.io.CharArrayWriter; +import java.nio.charset.StandardCharsets; import java.text.Normalizer; +import java.util.Arrays; + +public class StringUtil { -public class StringNormalizer { public static String normalizeStringForSearchIndexing(String theString) { + if (theString == null) { + return null; + } + CharArrayWriter outBuffer = new CharArrayWriter(theString.length()); /* @@ -51,4 +58,14 @@ public class StringNormalizer { } + public static String toUtf8String(byte[] theBytes) { + byte[] bytes = theBytes; + if (theBytes.length >= 3) { + if (theBytes[0] == -17 && theBytes[1] == -69 && theBytes[2] == -65) { + bytes = Arrays.copyOfRange(theBytes, 3, theBytes.length); + } + } + return new String(bytes, StandardCharsets.UTF_8); + } + } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/FhirVersionEnumTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/FhirVersionEnumTest.java new file mode 100644 index 00000000000..63051dfc65f --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/FhirVersionEnumTest.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.context; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class FhirVersionEnumTest { + + @Test + public void testGetVersionPermissive() { + assertEquals(FhirVersionEnum.DSTU2, FhirVersionEnum.forVersionString("1.0.0")); + assertEquals(FhirVersionEnum.DSTU2, FhirVersionEnum.forVersionString("1.0.1")); + + assertEquals(FhirVersionEnum.DSTU3, FhirVersionEnum.forVersionString("3.0.1")); + assertEquals(FhirVersionEnum.DSTU3, FhirVersionEnum.forVersionString("3.0.2")); + assertEquals(FhirVersionEnum.DSTU3, FhirVersionEnum.forVersionString("DSTU3")); + assertEquals(FhirVersionEnum.DSTU3, FhirVersionEnum.forVersionString("STU3")); + + assertEquals(FhirVersionEnum.R4, FhirVersionEnum.forVersionString("4.0.0")); + assertEquals(FhirVersionEnum.R4, FhirVersionEnum.forVersionString("4.0.1")); + assertEquals(FhirVersionEnum.R4, FhirVersionEnum.forVersionString("R4")); + + assertEquals(FhirVersionEnum.R5, FhirVersionEnum.forVersionString("R5")); + } + + +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java index c1b98297f41..6d20c21779e 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java @@ -173,6 +173,7 @@ public abstract class BaseApp { commands.add(new ExportConceptMapToCsvCommand()); commands.add(new ImportCsvToConceptMapCommand()); commands.add(new HapiFlywayMigrateDatabaseCommand()); + commands.add(new CreatePackageCommand()); return commands; } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/CreatePackageCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/CreatePackageCommand.java new file mode 100644 index 00000000000..123335e06b2 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/CreatePackageCommand.java @@ -0,0 +1,210 @@ +package ca.uhn.fhir.cli; + +/*- + * #%L + * HAPI FHIR - Command Line Client - API + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import com.google.common.io.Files; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.filefilter.FalseFileFilter; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.hl7.fhir.utilities.cache.PackageGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.print.attribute.standard.MediaSize; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.concurrent.ExecutionException; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@SuppressWarnings("UnstableApiUsage") +public class CreatePackageCommand extends BaseCommand { + private static final Logger ourLog = LoggerFactory.getLogger(CreatePackageCommand.class); + public static final String TARGET_DIRECTORY_OPT = "target-directory"; + public static final String DEPENDENCY_OPT = "dependency"; + public static final String INCLUDE_EXAMPLE_OPT = "include-example"; + public static final String INCLUDE_PACKAGE_OPT = "include-package"; + public static final String VERSION_OPT = "version"; + public static final String NAME_OPT = "name"; + public static final String DESCRIPTION_OPT = "description"; + private File myWorkDirectory; + private String myPackageName; + private String myPackageVersion; + private NpmPackage myPackage; + private String myPackageDescription; + + @Override + public String getCommandDescription() { + return "Create an NPM package using the FHIR packging format"; + } + + @Override + public String getCommandName() { + return "create-package"; + } + + @Override + public Options getOptions() { + Options options = new Options(); + addFhirVersionOption(options); + + addRequiredOption(options, null, NAME_OPT, "Package Name", "The name/id of the package, e.g. \"com.example.fhir.myapp\""); + addRequiredOption(options, null, VERSION_OPT, "Package Version", "The package version. FHIR packages use SemVer, e.g. \"1.0.0\""); + addOptionalOption(options, null, DESCRIPTION_OPT, "Description", "A description for this package"); + addOptionalOption(options, null, INCLUDE_PACKAGE_OPT, "File Spec", "A file spec to include in the package as a package resource/artifact"); + addOptionalOption(options, null, INCLUDE_EXAMPLE_OPT, "File Spec", "A file spec to include in the package as an example resource/artifact"); + addOptionalOption(options, null, TARGET_DIRECTORY_OPT, "Directory", "The directory in which to place the final package"); + addOptionalOption(options, null, DEPENDENCY_OPT, "name:version", "Include this dependency, in the form \"name:version\""); + + return options; + } + + @Override + public void cleanup() { + try { + if (myWorkDirectory != null) { + FileUtils.deleteDirectory(myWorkDirectory); + } + } catch (IOException e) { + throw new InternalErrorException("Failed to delete temporary directory \"" + myWorkDirectory.getAbsolutePath() + "\"", e); + } + } + + @Override + public void run(CommandLine theCommandLine) throws ParseException, ExecutionException { + + parseFhirContext(theCommandLine); + + myPackageName = theCommandLine.getOptionValue(NAME_OPT); + if (isBlank(myPackageName)) { + throw new ParseException("No package name supplied (--" + NAME_OPT + ")"); + } + if (!NpmPackage.isValidName(myPackageName)) { + throw new ParseException("Invalid package name: " + myPackageName); + } + + myPackageVersion = theCommandLine.getOptionValue(VERSION_OPT); + if (isBlank(myPackageVersion)) { + throw new ParseException("No package version supplied (--"+VERSION_OPT+")"); + } + if (!NpmPackage.isValidVersion(myPackageVersion)) { + throw new ParseException("Invalid package version: " + myPackageVersion); + } + + ourLog.info("Creating FHIR package {}#{}", myPackageName, myPackageVersion); + + PackageGenerator manifestGenerator = new PackageGenerator(); + manifestGenerator.name(myPackageName); + manifestGenerator.version(myPackageVersion); + manifestGenerator.description(myPackageDescription); + if (isNotBlank(myPackageDescription)) { + manifestGenerator.description(myPackageDescription); + } + + String[] dependencies = theCommandLine.getOptionValues(DEPENDENCY_OPT); + for (String nextDependencyString : dependencies) { + int colonIdx = nextDependencyString.indexOf(":"); + if (colonIdx == -1) { + throw new ParseException("Invalid dependency spec: " + nextDependencyString); + } + String depName = nextDependencyString.substring(0, colonIdx); + String depVersion = nextDependencyString.substring(colonIdx+1); + manifestGenerator.dependency(depName, depVersion); + } + + myWorkDirectory = Files.createTempDir(); + myPackage = NpmPackage.empty(manifestGenerator); + + ourLog.info("Using temporary directory: {}", myWorkDirectory.getAbsolutePath()); + + // Package + String[] packageValues = theCommandLine.getOptionValues(INCLUDE_PACKAGE_OPT); + String folder = "package"; + addFiles(packageValues, folder); + + // Example + packageValues = theCommandLine.getOptionValues(INCLUDE_EXAMPLE_OPT); + folder = "example"; + addFiles(packageValues, folder); + + + String targetDirectory = theCommandLine.getOptionValue(TARGET_DIRECTORY_OPT); + if (isBlank(targetDirectory)) { + targetDirectory = "."; + } + File targetFile = new File(new File(targetDirectory), myPackageName + "-" + myPackageVersion + ".tgz"); + + ourLog.info("Writing NPM file: {}", targetFile.toString()); + + try (FileOutputStream os = new FileOutputStream(targetFile, false)) { + myPackage.save(os); + } catch (IOException e) { + throw new ExecutionException("Failed to write file " + targetFile, e); + } + } + + public void addFiles(String[] thePackageValues, String theFolder) throws ParseException, ExecutionException { + if (thePackageValues != null) { + for (String nextPackageValue : thePackageValues) { + if (!nextPackageValue.contains("/")) { + throw new ParseException("Invalid file expression: " + nextPackageValue); + } + + int endIndex = nextPackageValue.lastIndexOf("/"); + String path = nextPackageValue.substring(0, endIndex); + String expression = nextPackageValue.substring(endIndex + 1); + IOFileFilter filter = new WildcardFileFilter(expression); + Collection files = FileUtils.listFiles(new File(path), filter, FalseFileFilter.INSTANCE); + + for (File next : files) { + + byte[] contentBytes; + String type; + try { + String contents = IOUtils.toString(new FileInputStream(next), StandardCharsets.UTF_8); + contentBytes = contents.getBytes(StandardCharsets.UTF_8); + type = EncodingEnum.detectEncoding(contents).newParser(myFhirCtx).parseResource(contents).fhirType(); + } catch (IOException | DataFormatException e) { + throw new ExecutionException("Failed to load/parse file: " + next.getName(), e); + } + + ourLog.info("Adding {} file of type {}: {}", theFolder, type, next.getName()); + myPackage.addFile(theFolder, next.getName(), contentBytes, type); + } + } + } + } +} + diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/CreatePackageCommandTest.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/CreatePackageCommandTest.java new file mode 100644 index 00000000000..d0ccd11974e --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/CreatePackageCommandTest.java @@ -0,0 +1,122 @@ +package ca.uhn.fhir.cli; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.test.BaseTest; +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.rauschig.jarchivelib.Archiver; +import org.rauschig.jarchivelib.ArchiverFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +public class CreatePackageCommandTest extends BaseTest { + + private static final Logger ourLog = LoggerFactory.getLogger(CreatePackageCommandTest.class); + private File myWorkDirectory; + private FhirContext myContext = FhirContext.forR4(); + private File myTargetDirectory; + private File myExtractDirectory; + + static { + System.setProperty("test", "true"); + } + + @Before + public void start() { + myWorkDirectory = Files.createTempDir(); + myTargetDirectory = Files.createTempDir(); + myExtractDirectory = Files.createTempDir(); + } + + @After + public void stop() { + try { + FileUtils.deleteDirectory(myWorkDirectory); + FileUtils.deleteDirectory(myTargetDirectory); + FileUtils.deleteDirectory(myExtractDirectory); + } catch (IOException e) { + throw new InternalErrorException("Failed to delete temporary directory \"" + myWorkDirectory.getAbsolutePath() + "\"", e); + } + } + + @Test + public void testCreatePackage() throws IOException { + + StructureDefinition sd = new StructureDefinition(); + sd.setUrl("http://foo/1"); + writeFile(sd, "foo1.json"); + + ValueSet vs = new ValueSet(); + vs.setUrl("http://foo/2"); + writeFile(vs, "foo2.json"); + + App.main(new String[]{ + "create-package", + "--fhir-version", "R4", + "--name", "com.example.ig", + "--version", "1.0.1", + "--include-package", myWorkDirectory.getAbsolutePath() + "/*.json", + "--target-directory", myTargetDirectory.getAbsolutePath(), + "--dependency", "hl7.fhir.core:4.0.1", + "--dependency", "foo.bar:1.2.3" + }); + + Archiver archiver = ArchiverFactory.createArchiver("tar", "gz"); + + File igArchive = new File(myTargetDirectory, "com.example.ig-1.0.1.tgz"); + archiver.extract(igArchive, myExtractDirectory); + + List allFiles = FileUtils.listFiles(myExtractDirectory, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE) + .stream() + .map(t -> t.getPath()) + .sorted() + .collect(Collectors.toList()); + ourLog.info("Archive contains files:\n * {}", allFiles.stream().collect(Collectors.joining("\n * "))); + + // Verify package.json + String packageJsonContents = IOUtils.toString(new FileInputStream(new File(myExtractDirectory, "package/package.json")), Charsets.UTF_8); + ourLog.info("Package.json:\n{}", packageJsonContents); + + String expectedPackageJson = "{\n" + + " \"name\": \"com.example.ig\",\n" + + " \"version\": \"1.0.1\",\n" + + " \"dependencies\": {\n" + + " \"hl7.fhir.core\": \"4.0.1\",\n" + + " \"foo.bar\": \"1.2.3\"\n" + + " }\n" + + "}"; + assertEquals(expectedPackageJson, packageJsonContents); + + // Try parsing the module again to make sure we can + NpmPackage loadedPackage = NpmPackage.fromPackage(new FileInputStream(igArchive)); + assertEquals("com.example.ig", loadedPackage.name()); + + } + + public void writeFile(IBaseResource theResource, String theFileName) throws IOException { + try (FileWriter w = new FileWriter(new File(myWorkDirectory, theFileName), false)) { + myContext.newJsonParser().encodeResourceToWriter(theResource, w); + } + } + +} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1895-fix-jpa-validation-performance-regression.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1895-fix-jpa-validation-performance-regression.yaml new file mode 100644 index 00000000000..d7c082340b9 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1895-fix-jpa-validation-performance-regression.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 1895 +title: "HAPI FHIR 5.0.0 introduced a regressin in JPA validator performance, where a number of unnecessary database lookups + were introduced. This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1911-add-package-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1911-add-package-support.yaml new file mode 100644 index 00000000000..e03aa2c5220 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1911-add-package-support.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1911 +title: "Support for FHIR NPM Packages has been added to the JPA server. This new functionality allows packages to + be installed to special tables within the FHIR JPA schema, and conformance resources used for validation." diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java index 1922f5176b6..32c0890eeb7 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDao.java @@ -163,7 +163,7 @@ public interface IFhirResourceDao extends IDao { /** * Read a resource by its internal PID */ - IBaseResource readByPid(ResourcePersistentId thePid); + T readByPid(ResourcePersistentId thePid); /** * @param theId diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index edadafdb0a8..0ce3f5c7f37 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -188,6 +188,10 @@ com.fasterxml.jackson.core jackson-databind + + io.swagger + swagger-annotations + com.helger diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 1feb96626bc..747e0f70bd5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -24,7 +24,11 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; -import ca.uhn.fhir.jpa.packages.IgInstallerSvc; +import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; +import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; +import ca.uhn.fhir.jpa.packages.JpaPackageCache; +import ca.uhn.fhir.jpa.packages.PackageInstallerSvcImpl; +import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl; @@ -56,6 +60,7 @@ import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import org.hibernate.jpa.HibernatePersistenceProvider; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.cache.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -117,15 +122,15 @@ import java.util.Date; public abstract class BaseConfig { public static final String JPA_VALIDATION_SUPPORT_CHAIN = "myJpaValidationSupportChain"; + public static final String JPA_VALIDATION_SUPPORT = "myJpaValidationSupport"; public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor"; public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider"; public static final String PERSISTED_JPA_BUNDLE_PROVIDER = "PersistedJpaBundleProvider"; public static final String PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH = "PersistedJpaBundleProvider_BySearch"; public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = "PersistedJpaSearchFirstPageBundleProvider"; - private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; public static final String SEARCH_BUILDER = "SearchBuilder"; public static final String HISTORY_BUILDER = "HistoryBuilder"; - + private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; @Autowired protected Environment myEnv; @Autowired @@ -202,6 +207,20 @@ public abstract class BaseConfig { return new DaoResourceLinkResolver(); } + @Bean + public IHapiPackageCacheManager packageCacheManager() { + JpaPackageCache retVal = new JpaPackageCache(); + retVal.getPackageServers().clear(); + retVal.getPackageServers().add(FilesystemPackageCacheManager.PRIMARY_SERVER); + retVal.getPackageServers().add(FilesystemPackageCacheManager.SECONDARY_SERVER); + return retVal; + } + + @Bean + public NpmJpaValidationSupport npmJpaValidationSupport() { + return new NpmJpaValidationSupport(); + } + @Bean public ISearchCacheSvc searchCacheSvc() { return new DatabaseSearchCacheSvcImpl(); @@ -266,7 +285,9 @@ public abstract class BaseConfig { } @Bean - public IgInstallerSvc igInstallerSvc() { return new IgInstallerSvc(); } + public IPackageInstallerSvc npmInstallerSvc() { + return new PackageInstallerSvcImpl(); + } @Bean public IConsentContextServices consentContextServices() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java index 8f27e2f0a41..f67f43c0c48 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java @@ -65,16 +65,16 @@ public abstract class BaseConfigDstu3Plus extends BaseConfig { public abstract ITermVersionAdapterSvc terminologyVersionAdapterSvc(); @Bean(name = "myDefaultProfileValidationSupport") - public IValidationSupport defaultProfileValidationSupport() { + public DefaultProfileValidationSupport defaultProfileValidationSupport() { return new DefaultProfileValidationSupport(fhirContext()); } @Bean(name = JPA_VALIDATION_SUPPORT_CHAIN) - public ValidationSupportChain jpaValidationSupportChain() { + public JpaValidationSupportChain jpaValidationSupportChain() { return new JpaValidationSupportChain(fhirContext()); } - @Bean(name = "myJpaValidationSupport") + @Bean(name = JPA_VALIDATION_SUPPORT) public IValidationSupport jpaValidationSupport() { return new JpaPersistedResourceValidationSupport(fhirContext()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java index 3dd5bb69a27..34e9ba7df84 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java @@ -86,28 +86,30 @@ public class BaseDstu2Config extends BaseConfig { @Bean(name = "myInstanceValidator") @Lazy - public IInstanceValidatorModule instanceValidator() { - ValidationSupportChain validationSupportChain = validationSupportChain(); - CachingValidationSupport cachingValidationSupport = new CachingValidationSupport(new HapiToHl7OrgDstu2ValidatingSupportWrapper(validationSupportChain)); + public IInstanceValidatorModule instanceValidator(ValidationSupportChain theValidationSupportChain) { + CachingValidationSupport cachingValidationSupport = new CachingValidationSupport(new HapiToHl7OrgDstu2ValidatingSupportWrapper(theValidationSupportChain)); FhirInstanceValidator retVal = new FhirInstanceValidator(cachingValidationSupport); retVal.setBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel.Warning); return retVal; } + @Bean(name = "myDefaultProfileValidationSupport") + public DefaultProfileValidationSupport defaultProfileValidationSupport() { + return new DefaultProfileValidationSupport(fhirContext()); + } + @Bean(name = JPA_VALIDATION_SUPPORT_CHAIN) - public ValidationSupportChain validationSupportChain() { - DefaultProfileValidationSupport defaultProfileValidationSupport = new DefaultProfileValidationSupport(fhirContext()); + public ValidationSupportChain validationSupportChain(DefaultProfileValidationSupport theDefaultProfileValidationSupport) { InMemoryTerminologyServerValidationSupport inMemoryTerminologyServer = new InMemoryTerminologyServerValidationSupport(fhirContextDstu2()); IValidationSupport jpaValidationSupport = jpaValidationSupportDstu2(); CommonCodeSystemsTerminologyService commonCodeSystemsTermSvc = new CommonCodeSystemsTerminologyService(fhirContext()); - return new ValidationSupportChain(defaultProfileValidationSupport, jpaValidationSupport, inMemoryTerminologyServer, commonCodeSystemsTermSvc); + return new ValidationSupportChain(theDefaultProfileValidationSupport, jpaValidationSupport, inMemoryTerminologyServer, commonCodeSystemsTermSvc); } @Primary - @Bean + @Bean(name = JPA_VALIDATION_SUPPORT) public IValidationSupport jpaValidationSupportDstu2() { - JpaPersistedResourceValidationSupport retVal = new JpaPersistedResourceValidationSupport(fhirContextDstu2()); - return retVal; + return new JpaPersistedResourceValidationSupport(fhirContextDstu2()); } @Bean(name = "myResourceCountsCache") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 560a2ae0adb..d19c9c4b229 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -932,7 +932,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override - public IBaseResource readByPid(ResourcePersistentId thePid) { + public T readByPid(ResourcePersistentId thePid) { StopWatch w = new StopWatch(); Optional entity = myResourceTableDao.findById(thePid.getIdAsLong()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index b1ba2102bb0..60111e1b662 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -99,6 +99,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import static ca.uhn.fhir.util.StringUtil.toUtf8String; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -768,14 +769,14 @@ public abstract class BaseTransactionProcessor { String matchUrl = toMatchUrl(nextReqEntry); matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); String patchBody = null; - String contentType = null; + String contentType; IBaseParameters patchBodyParameters = null; PatchTypeEnum patchType = null; if (res instanceof IBaseBinary) { IBaseBinary binary = (IBaseBinary) res; if (binary.getContent() != null && binary.getContent().length > 0) { - patchBody = new String(binary.getContent(), Charsets.UTF_8); + patchBody = toUtf8String(binary.getContent()); } contentType = binary.getContentType(); patchType = PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(myContext, contentType); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index 4af6519f71a..a37904f02d5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -28,6 +28,8 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.UriParam; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -43,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PostConstruct; import javax.transaction.Transactional; +import java.util.concurrent.TimeUnit; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -57,6 +60,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport private static final Logger ourLog = LoggerFactory.getLogger(JpaPersistedResourceValidationSupport.class); private final FhirContext myFhirContext; + private final IBaseResource myNoMatch; @Autowired private DaoRegistry myDaoRegistry; @@ -65,6 +69,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport private Class myValueSetType; private Class myQuestionnaireType; private Class myImplementationGuideType; + private Cache myLoadCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(); /** * Constructor @@ -73,6 +78,8 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport super(); Validate.notNull(theFhirContext); myFhirContext = theFhirContext; + + myNoMatch = myFhirContext.getResourceDefinition("Basic").newInstance(); } @@ -99,77 +106,86 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport return null; } - IdType id = new IdType(theUri); - boolean localReference = false; - if (id.hasBaseUrl() == false && id.hasIdPart() == true) { - localReference = true; - } + String key = theClass.getSimpleName() + " " + theUri; + IBaseResource fetched = myLoadCache.get(key, t -> { + IdType id = new IdType(theUri); + boolean localReference = false; + if (id.hasBaseUrl() == false && id.hasIdPart() == true) { + localReference = true; + } - String resourceName = myFhirContext.getResourceType(theClass); - IBundleProvider search; - if ("ValueSet".equals(resourceName)) { - if (localReference) { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(IAnyResource.SP_RES_ID, new StringParam(theUri)); - search = myDaoRegistry.getResourceDao("ValueSet").search(params); - if (search.size() == 0) { - params = new SearchParameterMap(); + String resourceName = myFhirContext.getResourceType(theClass); + IBundleProvider search; + if ("ValueSet".equals(resourceName)) { + if (localReference) { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(IAnyResource.SP_RES_ID, new StringParam(theUri)); + search = myDaoRegistry.getResourceDao("ValueSet").search(params); + if (search.size() == 0) { + params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(ValueSet.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao("ValueSet").search(params); + } + } else { + SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(1); params.add(ValueSet.SP_URL, new UriParam(theUri)); search = myDaoRegistry.getResourceDao("ValueSet").search(params); } - } else { + } else if ("StructureDefinition".equals(resourceName)) { + // Don't allow the core FHIR definitions to be overwritten + if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { + String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); + if (myFhirContext.getElementDefinition(typeName) != null) { + return myNoMatch; + } + } SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(1); - params.add(ValueSet.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao("ValueSet").search(params); - } - } else if ("StructureDefinition".equals(resourceName)) { - // Don't allow the core FHIR definitions to be overwritten - if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { - String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); - if (myFhirContext.getElementDefinition(typeName) != null) { - return null; + params.add(StructureDefinition.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); + } else if ("Questionnaire".equals(resourceName)) { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { + params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); + } else { + params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); } - } - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(StructureDefinition.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); - } else if ("Questionnaire".equals(resourceName)) { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { - params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); + search = myDaoRegistry.getResourceDao("Questionnaire").search(params); + } else if ("CodeSystem".equals(resourceName)) { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(CodeSystem.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao(resourceName).search(params); + } else if ("ImplementationGuide".equals(resourceName)) { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao("ImplementationGuide").search(params); } else { - params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); + throw new IllegalArgumentException("Can't fetch resource type: " + resourceName); } - search = myDaoRegistry.getResourceDao("Questionnaire").search(params); - } else if ("CodeSystem".equals(resourceName)) { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(CodeSystem.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao(resourceName).search(params); - } else if ("ImplementationGuide".equals(resourceName)) { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao("ImplementationGuide").search(params); - } else { - throw new IllegalArgumentException("Can't fetch resource type: " + resourceName); - } - Integer size = search.size(); - if (size == null || size == 0) { + Integer size = search.size(); + if (size == null || size == 0) { + return myNoMatch; + } + + if (size > 1) { + ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); + } + + return search.getResources(0, 1).get(0); + }); + + if (fetched == myNoMatch) { return null; } - if (size > 1) { - ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); - } - - return (T) search.getResources(0, 1).get(0); + return (T) fetched; } @Override @@ -192,4 +208,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport } + public void clearCaches() { + myLoadCache.invalidateAll(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 44a69c48bd9..4b55f0edbef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -1377,7 +1377,7 @@ public class SearchBuilder implements ISearchBuilder { return ResourcePersistentId.fromLongList(query.getResultList()); } - static Predicate[] toPredicateArray(List thePredicates) { + public static Predicate[] toPredicateArray(List thePredicates) { return thePredicates.toArray(new Predicate[0]); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageDao.java new file mode 100644 index 00000000000..d8f5fff9cdb --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageDao.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface INpmPackageDao extends JpaRepository { + + @Query("SELECT p FROM NpmPackageEntity p WHERE p.myPackageId = :id") + Optional findByPackageId(@Param("id") String thePackageId); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionDao.java new file mode 100644 index 00000000000..fc78e455312 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionDao.java @@ -0,0 +1,45 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface INpmPackageVersionDao extends JpaRepository { + + @Query("SELECT p FROM NpmPackageVersionEntity p WHERE p.myPackageId = :id") + Collection findByPackageId(@Param("id") String thePackageId); + + @Query("SELECT p FROM NpmPackageVersionEntity p WHERE p.myPackageId = :id AND p.myVersionId = :version") + Optional findByPackageIdAndVersion(@Param("id") String thePackageId, @Param("version") String thePackageVersion); + + /** + * Uses a "like" expression on the version ID + */ + @Query("SELECT p.myVersionId FROM NpmPackageVersionEntity p WHERE p.myPackageId = :id AND p.myVersionId like :version") + List findVersionIdsByPackageIdAndLikeVersion(@Param("id") String theId, @Param("version") String thePartialVersionString); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java new file mode 100644 index 00000000000..c1f5a40015e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/INpmPackageVersionResourceDao.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface INpmPackageVersionResourceDao extends JpaRepository { + + @Query("SELECT e FROM NpmPackageVersionResourceEntity e WHERE e.myCanonicalUrl = :url AND e.myFhirVersion = :fhirVersion AND e.myPackageVersion.myCurrentVersion = true") + Slice findCurrentVersionByCanonicalUrl(Pageable thePage, @Param("fhirVersion") FhirVersionEnum theFhirVersion, @Param("url") String theCanonicalUrl); + + @Query("SELECT e FROM NpmPackageVersionResourceEntity e WHERE e.myCanonicalUrl = :url AND e.myCanonicalVersion = :version AND e.myFhirVersion = :fhirVersion AND e.myPackageVersion.myCurrentVersion = true") + Slice findCurrentVersionByCanonicalUrlAndVersion(Pageable theOf, @Param("fhirVersion") FhirVersionEnum theFhirVersion, @Param("url") String theCanonicalUrl, @Param("version") String theCanonicalVersion); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index bae1abf40e2..3c7f568f351 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -41,6 +41,9 @@ import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSetConcept; import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation; import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; @@ -114,6 +117,9 @@ public class ExpungeEverythingService { counter.addAndGet(doExpungeEverythingQuery("UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null")); return null; }); + counter.addAndGet(expungeEverythingByType(NpmPackageVersionResourceEntity.class)); + counter.addAndGet(expungeEverythingByType(NpmPackageVersionEntity.class)); + counter.addAndGet(expungeEverythingByType(NpmPackageEntity.class)); counter.addAndGet(expungeEverythingByType(SearchParamPresent.class)); counter.addAndGet(expungeEverythingByType(ForcedId.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamDate.class)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java index 10d47bef3e2..4b4266e19ac 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java @@ -30,7 +30,7 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.util.StringNormalizer; +import ca.uhn.fhir.util.StringUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -131,7 +131,7 @@ class PredicateBuilderString extends BasePredicateBuilder implements IPredicateB } if (myDontUseHashesForSearch) { - String likeExpression = StringNormalizer.normalizeStringForSearchIndexing(rawSearchTerm); + String likeExpression = StringUtil.normalizeStringForSearchIndexing(rawSearchTerm); if (myDaoConfig.isAllowContainsSearches()) { if (theParameter instanceof StringParam) { if (((StringParam) theParameter).isContains()) { @@ -161,7 +161,7 @@ class PredicateBuilderString extends BasePredicateBuilder implements IPredicateB return theBuilder.equal(theFrom.get("myHashExact").as(Long.class), hash); } else { // Normalized Match - String normalizedString = StringNormalizer.normalizeStringForSearchIndexing(rawSearchTerm); + String normalizedString = StringUtil.normalizeStringForSearchIndexing(rawSearchTerm); String likeExpression; if ((theParameter instanceof StringParam) && (((((StringParam) theParameter).isContains()) && diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java index c87f76e3d3e..5b16f5811e9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java @@ -27,7 +27,19 @@ import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -127,6 +139,11 @@ public class TermCodeSystemVersion implements Serializable { return this; } + public TermCodeSystemVersion setId(Long theId) { + myId = theId; + return this; + } + @Override public boolean equals(Object theO) { if (this == theO) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java new file mode 100644 index 00000000000..ada6a0ef8b3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IHapiPackageCacheManager.java @@ -0,0 +1,98 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.cache.IPackageCacheManager; +import org.hl7.fhir.utilities.cache.NpmPackage; + +import java.io.IOException; +import java.util.Date; + +public interface IHapiPackageCacheManager extends IPackageCacheManager { + + NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) throws IOException; + + IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl); + + NpmPackageMetadataJson loadPackageMetadata(String thePackageId) throws ResourceNotFoundException; + + PackageContents loadPackageContents(String thePackageId, String theVersion); + + NpmPackageSearchResultJson search(PackageSearchSpec thePackageSearchSpec); + + PackageDeleteOutcomeJson uninstallPackage(String thePackageId, String theVersion); + + + class PackageContents { + + private byte[] myBytes; + private String myPackageId; + private String myVersion; + private Date myLastModified; + + /** + * Constructor + */ + public PackageContents() { + super(); + } + + public byte[] getBytes() { + return myBytes; + } + + public PackageContents setBytes(byte[] theBytes) { + myBytes = theBytes; + return this; + } + + public String getPackageId() { + return myPackageId; + } + + public PackageContents setPackageId(String thePackageId) { + myPackageId = thePackageId; + return this; + } + + public String getVersion() { + return myVersion; + } + + public PackageContents setVersion(String theVersion) { + myVersion = theVersion; + return this; + } + + public Date getLastModified() { + return myLastModified; + } + + public PackageContents setLastModified(Date theLastModified) { + myLastModified = theLastModified; + return this; + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IPackageInstallerSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IPackageInstallerSvc.java new file mode 100644 index 00000000000..d417a83c4dd --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IPackageInstallerSvc.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface IPackageInstallerSvc { + + PackageInstallOutcomeJson install(PackageInstallationSpec theSpec); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java new file mode 100644 index 00000000000..6b71e5f0258 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -0,0 +1,652 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.model.ExpungeOptions; +import ca.uhn.fhir.jpa.dao.data.INpmPackageDao; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; +import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.BinaryUtil; +import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.util.ResourceUtil; +import ca.uhn.fhir.util.StringUtil; +import org.apache.commons.collections4.comparators.ReverseComparator; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.utilities.cache.BasePackageCacheManager; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.transaction.Transactional; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.jpa.dao.SearchBuilder.toPredicateArray; +import static ca.uhn.fhir.util.StringUtil.toUtf8String; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class JpaPackageCache extends BasePackageCacheManager implements IHapiPackageCacheManager { + + public static final String UTF8_BOM = "\uFEFF"; + private static final Logger ourLog = LoggerFactory.getLogger(JpaPackageCache.class); + private final Map myVersionToContext = Collections.synchronizedMap(new HashMap<>()); + @PersistenceContext + protected EntityManager myEntityManager; + @Autowired + private INpmPackageDao myPackageDao; + @Autowired + private INpmPackageVersionDao myPackageVersionDao; + @Autowired + private INpmPackageVersionResourceDao myPackageVersionResourceDao; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private FhirContext myCtx; + @Autowired + private PlatformTransactionManager myTxManager; + + @Override + public NpmPackage loadPackageFromCacheOnly(String theId, @Nullable String theVersion) { + Optional packageVersion = loadPackageVersionEntity(theId, theVersion); + if (!packageVersion.isPresent() && theVersion.endsWith(".x")) { + String lookupVersion = theVersion; + do { + lookupVersion = lookupVersion.substring(0, lookupVersion.length() - 2); + } while (lookupVersion.endsWith(".x")); + + List candidateVersionIds = myPackageVersionDao.findVersionIdsByPackageIdAndLikeVersion(theId, lookupVersion + ".%"); + if (candidateVersionIds.size() > 0) { + candidateVersionIds.sort(PackageVersionComparator.INSTANCE); + packageVersion = loadPackageVersionEntity(theId, candidateVersionIds.get(candidateVersionIds.size() - 1)); + } + + } + + return packageVersion.map(t -> loadPackage(t)).orElse(null); + } + + private Optional loadPackageVersionEntity(String theId, @Nullable String theVersion) { + Validate.notBlank(theId, "theId must be populated"); + + Optional packageVersion = Optional.empty(); + if (isNotBlank(theVersion) && !"latest".equals(theVersion)) { + packageVersion = myPackageVersionDao.findByPackageIdAndVersion(theId, theVersion); + } else { + Optional pkg = myPackageDao.findByPackageId(theId); + if (pkg.isPresent()) { + packageVersion = myPackageVersionDao.findByPackageIdAndVersion(theId, pkg.get().getCurrentVersionId()); + } + } + return packageVersion; + } + + private NpmPackage loadPackage(NpmPackageVersionEntity thePackageVersion) { + PackageContents content = loadPackageContents(thePackageVersion); + ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes()); + try { + return NpmPackage.fromPackage(inputStream); + } catch (IOException e) { + throw new InternalErrorException(e); + } + } + + private IHapiPackageCacheManager.PackageContents loadPackageContents(NpmPackageVersionEntity thePackageVersion) { + IFhirResourceDao binaryDao = getBinaryDao(); + IBaseBinary binary = binaryDao.readByPid(new ResourcePersistentId(thePackageVersion.getPackageBinary().getId())); + + PackageContents retVal = new PackageContents() + .setBytes(binary.getContent()) + .setPackageId(thePackageVersion.getPackageId()) + .setVersion(thePackageVersion.getVersionId()) + .setLastModified(thePackageVersion.getUpdatedTime()); + return retVal; + } + + @SuppressWarnings("unchecked") + private IFhirResourceDao getBinaryDao() { + return myDaoRegistry.getResourceDao("Binary"); + } + + @Override + public NpmPackage addPackageToCache(String thePackageId, String thePackageVersionId, InputStream thePackageTgzInputStream, String theSourceDesc) throws IOException { + Validate.notBlank(thePackageId, "thePackageId must not be null"); + Validate.notBlank(thePackageVersionId, "thePackageVersionId must not be null"); + Validate.notNull(thePackageTgzInputStream, "thePackageTgzInputStream must not be null"); + + byte[] bytes = IOUtils.toByteArray(thePackageTgzInputStream); + + NpmPackage npmPackage = NpmPackage.fromPackage(new ByteArrayInputStream(bytes)); + if (!npmPackage.id().equals(thePackageId)) { + throw new InvalidRequestException("Package ID " + npmPackage.id() + " doesn't match expected: " + thePackageId); + } + if (!PackageVersionComparator.isEquivalent(thePackageVersionId, npmPackage.version())) { + throw new InvalidRequestException("Package ID " + npmPackage.version() + " doesn't match expected: " + thePackageVersionId); + } + + String packageVersionId = npmPackage.version(); + FhirVersionEnum fhirVersion = FhirVersionEnum.forVersionString(npmPackage.fhirVersion()); + if (fhirVersion == null) { + throw new InvalidRequestException("Unknown FHIR version: " + npmPackage.fhirVersion()); + } + FhirContext packageContext = getFhirContext(fhirVersion); + + IBaseBinary binary = createPackageBinary(bytes); + + return newTxTemplate().execute(tx -> { + + ResourceTable persistedPackage = (ResourceTable) getBinaryDao().create(binary).getEntity(); + NpmPackageEntity pkg = myPackageDao.findByPackageId(thePackageId).orElseGet(() -> createPackage(npmPackage)); + NpmPackageVersionEntity packageVersion = myPackageVersionDao.findByPackageIdAndVersion(thePackageId, packageVersionId).orElse(null); + if (packageVersion != null) { + NpmPackage existingPackage = loadPackageFromCacheOnly(packageVersion.getPackageId(), packageVersion.getVersionId()); + String msg = "Package version already exists in local storage, no action taken: " + thePackageId + "#" + packageVersionId; + getProcessingMessages(existingPackage).add(msg); + ourLog.info(msg); + return existingPackage; + } + + boolean currentVersion = updateCurrentVersionFlagForAllPackagesBasedOnNewIncomingVersion(thePackageId, packageVersionId); + if (currentVersion) { + getProcessingMessages(npmPackage).add("Marking package " + thePackageId + "#" + thePackageVersionId + " as current version"); + pkg.setCurrentVersionId(packageVersionId); + pkg.setDescription(npmPackage.description()); + myPackageDao.save(pkg); + } else { + getProcessingMessages(npmPackage).add("Package " + thePackageId + "#" + thePackageVersionId + " is not the newest version"); + } + + packageVersion = new NpmPackageVersionEntity(); + packageVersion.setPackageId(thePackageId); + packageVersion.setVersionId(packageVersionId); + packageVersion.setPackage(pkg); + packageVersion.setPackageBinary(persistedPackage); + packageVersion.setSavedTime(new Date()); + packageVersion.setDescription(npmPackage.description()); + packageVersion.setFhirVersionId(npmPackage.fhirVersion()); + packageVersion.setFhirVersion(fhirVersion); + packageVersion.setCurrentVersion(currentVersion); + packageVersion.setPackageSizeBytes(bytes.length); + packageVersion = myPackageVersionDao.save(packageVersion); + + String dirName = "package"; + NpmPackage.NpmPackageFolder packageFolder = npmPackage.getFolders().get(dirName); + for (Map.Entry> nextTypeToFiles : packageFolder.getTypes().entrySet()) { + String nextType = nextTypeToFiles.getKey(); + for (String nextFile : nextTypeToFiles.getValue()) { + + byte[] contents; + String contentsString; + try { + contents = packageFolder.fetchFile(nextFile); + contentsString = toUtf8String(contents); + } catch (IOException e) { + throw new InternalErrorException(e); + } + + IBaseResource resource; + if (nextFile.toLowerCase().endsWith(".xml")) { + resource = packageContext.newXmlParser().parseResource(contentsString); + } else if (nextFile.toLowerCase().endsWith(".json")) { + resource = packageContext.newJsonParser().parseResource(contentsString); + } else { + getProcessingMessages(npmPackage).add("Not indexing file: " + nextFile); + continue; + } + + /* + * Re-encode the resource as JSON with the narrative removed in order to reduce the footprint. + * This is useful since we'll be loading these resources back and hopefully keeping lots of + * them in memory in order to speed up validation activities. + */ + String contentType = Constants.CT_FHIR_JSON_NEW; + ResourceUtil.removeNarrative(packageContext, resource); + byte[] minimizedContents = packageContext.newJsonParser().encodeResourceToString(resource).getBytes(StandardCharsets.UTF_8); + + IBaseBinary resourceBinary = createPackageResourceBinary(nextFile, minimizedContents, contentType); + ResourceTable persistedResource = (ResourceTable) getBinaryDao().create(resourceBinary).getEntity(); + + NpmPackageVersionResourceEntity resourceEntity = new NpmPackageVersionResourceEntity(); + resourceEntity.setPackageVersion(packageVersion); + resourceEntity.setResourceBinary(persistedResource); + resourceEntity.setDirectory(dirName); + resourceEntity.setFhirVersionId(npmPackage.fhirVersion()); + resourceEntity.setFhirVersion(fhirVersion); + resourceEntity.setFilename(nextFile); + resourceEntity.setResourceType(nextType); + resourceEntity.setResSizeBytes(contents.length); + BaseRuntimeChildDefinition urlChild = packageContext.getResourceDefinition(nextType).getChildByName("url"); + BaseRuntimeChildDefinition versionChild = packageContext.getResourceDefinition(nextType).getChildByName("version"); + String url = null; + String version = null; + if (urlChild != null) { + url = urlChild.getAccessor().getFirstValueOrNull(resource).map(t -> ((IPrimitiveType) t).getValueAsString()).orElse(null); + resourceEntity.setCanonicalUrl(url); + version = versionChild.getAccessor().getFirstValueOrNull(resource).map(t -> ((IPrimitiveType) t).getValueAsString()).orElse(null); + resourceEntity.setCanonicalVersion(version); + } + myPackageVersionResourceDao.save(resourceEntity); + + String msg = "Indexing Resource[" + dirName + '/' + nextFile + "] with URL: " + defaultString(url) + "|" + defaultString(version); + getProcessingMessages(npmPackage).add(msg); + ourLog.info("Package[{}#{}] " + msg, thePackageId, packageVersionId); + } + } + + getProcessingMessages(npmPackage).add("Successfully added package " + npmPackage.id() + "#" + npmPackage.version() + " to registry"); + + return npmPackage; + }); + + } + + private boolean updateCurrentVersionFlagForAllPackagesBasedOnNewIncomingVersion(String thePackageId, String thePackageVersion) { + Collection existingVersions = myPackageVersionDao.findByPackageId(thePackageId); + boolean retVal = true; + + for (NpmPackageVersionEntity next : existingVersions) { + int cmp = PackageVersionComparator.INSTANCE.compare(next.getVersionId(), thePackageVersion); + assert cmp != 0; + if (cmp < 0) { + if (next.isCurrentVersion()) { + next.setCurrentVersion(false); + myPackageVersionDao.save(next); + } + } else { + retVal = false; + } + } + + return retVal; + } + + @Nonnull + public FhirContext getFhirContext(FhirVersionEnum theFhirVersion) { + return myVersionToContext.computeIfAbsent(theFhirVersion, v -> new FhirContext(v)); + } + + private IBaseBinary createPackageBinary(byte[] theBytes) { + IBaseBinary binary = BinaryUtil.newBinary(myCtx); + BinaryUtil.setData(myCtx, binary, theBytes, Constants.CT_APPLICATION_GZIP); + return binary; + } + + private IBaseBinary createPackageResourceBinary(String theFileName, byte[] theBytes, String theContentType) { + IBaseBinary binary = BinaryUtil.newBinary(myCtx); + BinaryUtil.setData(myCtx, binary, theBytes, theContentType); + return binary; + } + + private NpmPackageEntity createPackage(NpmPackage theNpmPackage) { + NpmPackageEntity entity = new NpmPackageEntity(); + entity.setPackageId(theNpmPackage.id()); + entity.setCurrentVersionId(theNpmPackage.version()); + return myPackageDao.save(entity); + } + + @Override + public NpmPackage loadPackage(String thePackageId, String thePackageVersion) throws FHIRException, IOException { + NpmPackage cachedPackage = loadPackageFromCacheOnly(thePackageId, thePackageVersion); + if (cachedPackage != null) { + return cachedPackage; + } + + InputStreamWithSrc pkg = super.loadFromPackageServer(thePackageId, thePackageVersion); + if (pkg == null) { + throw new ResourceNotFoundException("Unable to locate package " + thePackageId + "#" + thePackageVersion); + } + + try { + NpmPackage retVal = addPackageToCache(thePackageId, thePackageVersion == null ? pkg.version : thePackageVersion, pkg.stream, pkg.url); + getProcessingMessages(retVal).add(0, "Package fetched from server at: " + pkg.url); + return retVal; + } finally { + pkg.stream.close(); + } + + } + + private TransactionTemplate newTxTemplate() { + return new TransactionTemplate(myTxManager); + } + + @Override + public NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) throws IOException { + Validate.notBlank(theInstallationSpec.getName(), "thePackageId must not be blank"); + Validate.notBlank(theInstallationSpec.getVersion(), "thePackageVersion must not be blank"); + + if (isNotBlank(theInstallationSpec.getPackageUrl())) { + byte[] contents = loadPackageUrlContents(theInstallationSpec.getPackageUrl()); + theInstallationSpec.setPackageContents(contents); + } + + if (theInstallationSpec.getPackageContents() != null) { + return addPackageToCache(theInstallationSpec.getName(), theInstallationSpec.getVersion(), new ByteArrayInputStream(theInstallationSpec.getPackageContents()), "Manually added"); + } + + return loadPackage(theInstallationSpec.getName(), theInstallationSpec.getVersion()); + } + + protected byte[] loadPackageUrlContents(String thePackageUrl) { + if (thePackageUrl.startsWith("classpath:")) { + return ClasspathUtil.loadResourceAsByteArray(thePackageUrl.substring("classpath:".length())); + } else { + HttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(); + try (CloseableHttpResponse request = HttpClientBuilder + .create() + .setConnectionManager(connManager) + .build() + .execute(new HttpGet(thePackageUrl))) { + if (request.getStatusLine().getStatusCode() != 200) { + throw new IOException("Received HTTP " + request.getStatusLine().getStatusCode()); + } + return IOUtils.toByteArray(request.getEntity().getContent()); + } catch (IOException e) { + throw new InternalErrorException("Error loading \"" + thePackageUrl + "\": " + e.getMessage()); + } + } + } + + @Override + @Transactional + public IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl) { + + String canonicalUrl = theCanonicalUrl; + + int versionSeparator = canonicalUrl.lastIndexOf('|'); + Slice slice; + if (versionSeparator != -1) { + String canonicalVersion = canonicalUrl.substring(versionSeparator + 1); + canonicalUrl = canonicalUrl.substring(0, versionSeparator); + slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersion(PageRequest.of(0, 1), theFhirVersion, canonicalUrl, canonicalVersion); + } else { + slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(PageRequest.of(0, 1), theFhirVersion, canonicalUrl); + } + + if (slice.isEmpty()) { + return null; + } else { + NpmPackageVersionResourceEntity contents = slice.getContent().get(0); + ResourcePersistentId binaryPid = new ResourcePersistentId(contents.getResourceBinary().getId()); + IBaseBinary binary = getBinaryDao().readByPid(binaryPid); + byte[] resourceContentsBytes = BinaryUtil.getOrCreateData(myCtx, binary).getValue(); + String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); + + FhirContext packageContext = getFhirContext(contents.getFhirVersion()); + return EncodingEnum.detectEncoding(resourceContents).newParser(packageContext).parseResource(resourceContents); + } + } + + @Override + @Transactional + public NpmPackageMetadataJson loadPackageMetadata(String thePackageId) { + NpmPackageMetadataJson retVal = new NpmPackageMetadataJson(); + + Optional pkg = myPackageDao.findByPackageId(thePackageId); + if (!pkg.isPresent()) { + throw new ResourceNotFoundException("Unknown package ID: " + thePackageId); + } + + List packageVersions = new ArrayList<>(myPackageVersionDao.findByPackageId(thePackageId)); + packageVersions.sort(new ReverseComparator<>((o1, o2) -> PackageVersionComparator.INSTANCE.compare(o1.getVersionId(), o2.getVersionId()))); + + for (NpmPackageVersionEntity next : packageVersions) { + if (next.isCurrentVersion()) { + retVal.setDistTags(new NpmPackageMetadataJson.DistTags().setLatest(next.getVersionId())); + } + + NpmPackageMetadataJson.Version version = new NpmPackageMetadataJson.Version(); + version.setFhirVersion(next.getFhirVersionId()); + version.setDescription(next.getDescription()); + version.setName(next.getPackageId()); + version.setVersion(next.getVersionId()); + version.setBytes(next.getPackageSizeBytes()); + retVal.addVersion(version); + + } + + return retVal; + } + + @Override + @Transactional + public PackageContents loadPackageContents(String thePackageId, String theVersion) { + Optional entity = loadPackageVersionEntity(thePackageId, theVersion); + return entity.map(t -> loadPackageContents(t)).orElse(null); + } + + @Override + @Transactional + public NpmPackageSearchResultJson search(PackageSearchSpec thePackageSearchSpec) { + NpmPackageSearchResultJson retVal = new NpmPackageSearchResultJson(); + + CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); + + // Query for total + { + CriteriaQuery countCriteriaQuery = cb.createQuery(Long.class); + Root countCriteriaRoot = countCriteriaQuery.from(NpmPackageVersionEntity.class); + countCriteriaQuery.multiselect(cb.countDistinct(countCriteriaRoot.get("myPackageId"))); + + List predicates = createSearchPredicates(thePackageSearchSpec, cb, countCriteriaRoot); + + countCriteriaQuery.where(toPredicateArray(predicates)); + Long total = myEntityManager.createQuery(countCriteriaQuery).getSingleResult(); + retVal.setTotal(Math.toIntExact(total)); + } + + // Query for results + { + CriteriaQuery criteriaQuery = cb.createQuery(NpmPackageVersionEntity.class); + Root root = criteriaQuery.from(NpmPackageVersionEntity.class); + + List predicates = createSearchPredicates(thePackageSearchSpec, cb, root); + + criteriaQuery.where(toPredicateArray(predicates)); + criteriaQuery.orderBy(cb.asc(root.get("myPackageId"))); + TypedQuery query = myEntityManager.createQuery(criteriaQuery); + query.setFirstResult(thePackageSearchSpec.getStart()); + query.setMaxResults(thePackageSearchSpec.getSize()); + + List resultList = query.getResultList(); + for (NpmPackageVersionEntity next : resultList) { + + if (!retVal.hasPackageWithId(next.getPackageId())) { + retVal + .addObject() + .getPackage() + .setName(next.getPackageId()) + .setDescription(next.getPackage().getDescription()) + .setVersion(next.getVersionId()) + .addFhirVersion(next.getFhirVersionId()) + .setBytes(next.getPackageSizeBytes()); + } else { + NpmPackageSearchResultJson.Package retPackage = retVal.getPackageWithId(next.getPackageId()); + retPackage.addFhirVersion(next.getFhirVersionId()); + + int cmp = PackageVersionComparator.INSTANCE.compare(next.getVersionId(), retPackage.getVersion()); + if (cmp > 0) { + retPackage.setVersion(next.getVersionId()); + } + + } + } + + } + + + return retVal; + } + + @Override + @Transactional + public PackageDeleteOutcomeJson uninstallPackage(String thePackageId, String theVersion) { + PackageDeleteOutcomeJson retVal = new PackageDeleteOutcomeJson(); + + Optional packageVersion = myPackageVersionDao.findByPackageIdAndVersion(thePackageId, theVersion); + if (packageVersion.isPresent()) { + + String msg = "Deleting package " + thePackageId + "#" + theVersion; + ourLog.info(msg); + retVal.getMessage().add(msg); + + for (NpmPackageVersionResourceEntity next : packageVersion.get().getResources()) { + msg = "Deleting package +" + thePackageId + "#" + theVersion + "resource: " + next.getCanonicalUrl(); + ourLog.info(msg); + retVal.getMessage().add(msg); + + myPackageVersionResourceDao.delete(next); + + ExpungeOptions options = new ExpungeOptions(); + options.setExpungeDeletedResources(true).setExpungeOldVersions(true); + getBinaryDao().delete(next.getResourceBinary().getIdDt().toVersionless()); + getBinaryDao().forceExpungeInExistingTransaction(next.getResourceBinary().getIdDt().toVersionless(), options, null); + } + + myPackageVersionDao.delete(packageVersion.get()); + + ExpungeOptions options = new ExpungeOptions(); + options.setExpungeDeletedResources(true).setExpungeOldVersions(true); + getBinaryDao().delete(packageVersion.get().getPackageBinary().getIdDt().toVersionless()); + getBinaryDao().forceExpungeInExistingTransaction(packageVersion.get().getPackageBinary().getIdDt().toVersionless(), options, null); + + Collection remainingVersions = myPackageVersionDao.findByPackageId(thePackageId); + if (remainingVersions.size() == 0) { + msg = "No versions of package " + thePackageId + " remain"; + ourLog.info(msg); + retVal.getMessage().add(msg); + Optional pkgEntity = myPackageDao.findByPackageId(thePackageId); + myPackageDao.delete(pkgEntity.get()); + } else { + + List versions = remainingVersions + .stream() + .sorted((o1, o2) -> PackageVersionComparator.INSTANCE.compare(o1.getVersionId(), o2.getVersionId())) + .collect(Collectors.toList()); + for (int i = 0; i < versions.size(); i++) { + boolean isCurrent = i == versions.size() - 1; + if (isCurrent != versions.get(i).isCurrentVersion()) { + versions.get(i).setCurrentVersion(isCurrent); + myPackageVersionDao.save(versions.get(i)); + } + } + + } + + } else { + + String msg = "No package found with the given ID"; + retVal.getMessage().add(msg); + + } + + return retVal; + } + + @Nonnull + public List createSearchPredicates(PackageSearchSpec thePackageSearchSpec, CriteriaBuilder theCb, Root theRoot) { + List predicates = new ArrayList<>(); + + if (isNotBlank(thePackageSearchSpec.getResourceUrl())) { + Join resources = theRoot.join("myResources", JoinType.LEFT); + + predicates.add(theCb.equal(resources.get("myCanonicalUrl").as(String.class), thePackageSearchSpec.getResourceUrl())); + } + + if (isNotBlank(thePackageSearchSpec.getDescription())) { + String searchTerm = "%" + thePackageSearchSpec.getDescription() + "%"; + searchTerm = StringUtil.normalizeStringForSearchIndexing(searchTerm); + predicates.add(theCb.like(theRoot.get("myDescriptionUpper").as(String.class), searchTerm)); + } + + if (isNotBlank(thePackageSearchSpec.getFhirVersion())) { + if (!thePackageSearchSpec.getFhirVersion().matches("([0-9]+\\.)+[0-9]+")) { + FhirVersionEnum versionEnum = FhirVersionEnum.forVersionString(thePackageSearchSpec.getFhirVersion()); + if (versionEnum != null) { + predicates.add(theCb.equal(theRoot.get("myFhirVersion").as(FhirVersionEnum.class), versionEnum)); + } + } else { + predicates.add(theCb.like(theRoot.get("myFhirVersionId").as(String.class), thePackageSearchSpec.getFhirVersion() + "%")); + } + } + + return predicates; + } + + @SuppressWarnings("unchecked") + public static List getProcessingMessages(NpmPackage thePackage) { + return (List) thePackage.getUserData().computeIfAbsent("JpPackageCache_ProcessingMessages", t -> new ArrayList<>()); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java new file mode 100644 index 00000000000..5c9b0d7693e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmJpaValidationSupport.java @@ -0,0 +1,71 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.IValidationSupport; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Nullable; + +public class NpmJpaValidationSupport implements IValidationSupport { + + @Autowired + private FhirContext myFhirContext; + + @Autowired + private IHapiPackageCacheManager myHapiPackageCacheManager; + + @Override + public FhirContext getFhirContext() { + return myFhirContext; + } + + @Override + public IBaseResource fetchValueSet(String theUri) { + return fetchResource("ValueSet", theUri); + } + + @Override + public IBaseResource fetchCodeSystem(String theUri) { + return fetchResource("CodeSystem", theUri); + } + + @Override + public IBaseResource fetchStructureDefinition(String theUri) { + return fetchResource("StructureDefinition", theUri); + } + + @Nullable + public IBaseResource fetchResource(String theResourceType, String theUri) { + FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion(); + IBaseResource asset = myHapiPackageCacheManager.loadPackageAssetByUrl(fhirVersion, theUri); + if (asset != null) { + Class type = myFhirContext.getResourceDefinition(theResourceType).getImplementingClass(); + if (type.isAssignableFrom(asset.getClass())) { + return asset; + } + } + return null; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataJson.java new file mode 100644 index 00000000000..50b16ffdd50 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageMetadataJson.java @@ -0,0 +1,155 @@ +package ca.uhn.fhir.jpa.packages; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.util.JsonDateDeserializer; +import ca.uhn.fhir.jpa.util.JsonDateSerializer; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.annotation.Nonnull; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +@ApiModel("Represents an NPM package metadata response") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) +public class NpmPackageMetadataJson { + + @JsonProperty("dist-tags") + private DistTags myDistTags; + @JsonProperty("modified") + @JsonSerialize(using = JsonDateSerializer.class) + @JsonDeserialize(using = JsonDateDeserializer.class) + private Date myModified; + @JsonProperty("name") + private String myName; + @JsonProperty("versions") + private Map myVersionIdToVersion; + + public void addVersion(Version theVersion) { + getVersions().put(theVersion.getVersion(), theVersion); + } + + @Nonnull + public Map getVersions() { + if (myVersionIdToVersion == null) { + myVersionIdToVersion = new LinkedHashMap<>(); + } + return myVersionIdToVersion; + } + + public DistTags getDistTags() { + return myDistTags; + } + + public void setDistTags(DistTags theDistTags) { + myDistTags = theDistTags; + } + + public void setModified(Date theModified) { + myModified = theModified; + } + + public void setName(String theName) { + myName = theName; + } + + + public static class DistTags { + + @JsonProperty("latest") + private String myLatest; + + public String getLatest() { + return myLatest; + } + + public DistTags setLatest(String theLatest) { + myLatest = theLatest; + return this; + } + } + + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) + public static class Version { + + @JsonProperty("name") + private String myName; + @JsonProperty("version") + private String myVersion; + @JsonProperty("description") + private String myDescription; + @JsonProperty("fhirVersion") + private String myFhirVersion; + @ApiModelProperty(value = "The size of this package in bytes", example = "1000") + @JsonProperty("_bytes") + private long myBytes; + + public String getName() { + return myName; + } + + public void setName(String theName) { + myName = theName; + } + + public String getDescription() { + return myDescription; + } + + public void setDescription(String theDescription) { + myDescription = theDescription; + } + + public String getFhirVersion() { + return myFhirVersion; + } + + public void setFhirVersion(String theFhirVersion) { + myFhirVersion = theFhirVersion; + } + + public String getVersion() { + return myVersion; + } + + public void setVersion(String theVersion) { + myVersion = theVersion; + } + + public long getBytes() { + return myBytes; + } + + public void setBytes(long theBytes) { + myBytes = theBytes; + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageSearchResultJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageSearchResultJson.java new file mode 100644 index 00000000000..004771ea69c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/NpmPackageSearchResultJson.java @@ -0,0 +1,153 @@ +package ca.uhn.fhir.jpa.packages; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.ArrayList; +import java.util.List; + +@ApiModel("Represents an NPM package search response") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) +public class NpmPackageSearchResultJson { + + @JsonProperty("objects") + private List myObjects; + @JsonProperty("total") + private int myTotal; + + public List getObjects() { + if (myObjects == null) { + myObjects = new ArrayList<>(); + } + return myObjects; + } + + public ObjectElement addObject() { + ObjectElement object = new ObjectElement(); + getObjects().add(object); + return object; + } + + public int getTotal() { + return myTotal; + } + + public void setTotal(int theTotal) { + myTotal = theTotal; + } + + public boolean hasPackageWithId(String thePackageId) { + return getObjects().stream().anyMatch(t -> t.getPackage().getName().equals(thePackageId)); + } + + public Package getPackageWithId(String thePackageId) { + return getObjects().stream().map(t -> t.getPackage()).filter(t -> t.getName().equals(thePackageId)).findFirst().orElseThrow(() -> new IllegalArgumentException()); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) + public static class ObjectElement { + + @JsonProperty("package") + private Package myPackage; + + public Package getPackage() { + if (myPackage == null) { + myPackage = new Package(); + } + return myPackage; + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) + public static class Package { + + @JsonProperty("name") + private String myName; + @JsonProperty("version") + private String myVersion; + @JsonProperty("description") + private String myDescription; + @JsonProperty("fhirVersion") + private List myFhirVersion; + @ApiModelProperty(value = "The size of this package in bytes", example = "1000") + @JsonProperty("_bytes") + private long myBytes; + + public long getBytes() { + return myBytes; + } + + public Package setBytes(long theBytes) { + myBytes = theBytes; + return this; + } + + public String getName() { + return myName; + } + + public Package setName(String theName) { + myName = theName; + return this; + } + + public String getDescription() { + return myDescription; + } + + public Package setDescription(String theDescription) { + myDescription = theDescription; + return this; + } + + public List getFhirVersion() { + if (myFhirVersion == null) { + myFhirVersion = new ArrayList<>(); + } + return myFhirVersion; + } + + public String getVersion() { + return myVersion; + } + + public Package setVersion(String theVersion) { + myVersion = theVersion; + return this; + } + + public Package addFhirVersion(String theFhirVersionId) { + if (!getFhirVersion().contains(theFhirVersionId)) { + getFhirVersion().add(theFhirVersionId); + getFhirVersion().sort(PackageVersionComparator.INSTANCE); + } + return this; + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageDeleteOutcomeJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageDeleteOutcomeJson.java new file mode 100644 index 00000000000..25af72948d8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageDeleteOutcomeJson.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.jpa.packages; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; + +import java.util.ArrayList; +import java.util.List; + +@ApiModel("Represents an NPM package deletion response") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) +public class PackageDeleteOutcomeJson { + + @JsonProperty("messages") + private List myMessage; + + public List getMessage() { + if (myMessage == null) { + myMessage = new ArrayList<>(); + } + return myMessage; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallOutcomeJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallOutcomeJson.java new file mode 100644 index 00000000000..8f2018f905a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallOutcomeJson.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.jpa.packages; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; + +import java.util.ArrayList; +import java.util.List; + +@ApiModel("Represents an NPM package installation response") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) +public class PackageInstallOutcomeJson { + + @JsonProperty("messages") + private List myMessage; + + public List getMessage() { + if (myMessage == null) { + myMessage = new ArrayList<>(); + } + return myMessage; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallationSpec.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallationSpec.java new file mode 100644 index 00000000000..7a41ae5eebc --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallationSpec.java @@ -0,0 +1,196 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + + +import ca.uhn.fhir.model.api.annotation.ExampleSupplier; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +@ApiModel( + value = "PackageInstallationSpec", + description = + "Defines a set of instructions for package installation" +) +@JsonPropertyOrder({ + "name", "version", "packageUrl", "installMode", "installResourceTypes", "validationMode" +}) +@ExampleSupplier({PackageInstallationSpec.ExampleSupplier.class, PackageInstallationSpec.ExampleSupplier2.class}) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) +public class PackageInstallationSpec { + + @ApiModelProperty("The direct package URL") + @JsonProperty("packageUrl") + private String myPackageUrl; + @ApiModelProperty("The NPM package Name") + @JsonProperty("name") + private String myName; + @ApiModelProperty("The direct package version") + @JsonProperty("version") + private String myVersion; + @ApiModelProperty("Should resources from this package be extracted from the package and installed into the repository individually") + @JsonProperty("installMode") + private InstallModeEnum myInstallMode; + @ApiModelProperty("If resources are being installed individually, this is list provides the resource types to install. By default, all conformance resources will be installed.") + @JsonProperty("installResourceTypes") + private List myInstallResourceTypes; + @ApiModelProperty("Should dependencies be automatically resolved, fetched and installed with the same settings") + @JsonProperty("fetchDependencies") + private boolean myFetchDependencies; + @ApiModelProperty("Any values provided here will be interpreted as a regex. Dependencies with an ID matching any regex will be skipped.") + private List myDependencyExcludes; + @JsonIgnore + private byte[] myPackageContents; + + public List getDependencyExcludes() { + if (myDependencyExcludes == null) { + myDependencyExcludes = new ArrayList<>(); + } + return myDependencyExcludes; + } + + public void setDependencyExcludes(List theDependencyExcludes) { + myDependencyExcludes = theDependencyExcludes; + } + + public boolean isFetchDependencies() { + return myFetchDependencies; + } + + public PackageInstallationSpec setFetchDependencies(boolean theFetchDependencies) { + myFetchDependencies = theFetchDependencies; + return this; + } + + public String getPackageUrl() { + return myPackageUrl; + } + + public PackageInstallationSpec setPackageUrl(String thePackageUrl) { + myPackageUrl = thePackageUrl; + return this; + } + + public InstallModeEnum getInstallMode() { + return myInstallMode; + } + + public PackageInstallationSpec setInstallMode(InstallModeEnum theInstallMode) { + myInstallMode = theInstallMode; + return this; + } + + public List getInstallResourceTypes() { + if (myInstallResourceTypes == null) { + myInstallResourceTypes = new ArrayList<>(); + } + return myInstallResourceTypes; + } + + public void setInstallResourceTypes(List theInstallResourceTypes) { + myInstallResourceTypes = theInstallResourceTypes; + } + + public String getName() { + return myName; + } + + public PackageInstallationSpec setName(String theName) { + myName = theName; + return this; + } + + public String getVersion() { + return myVersion; + } + + public PackageInstallationSpec setVersion(String theVersion) { + myVersion = theVersion; + return this; + } + + public byte[] getPackageContents() { + return myPackageContents; + } + + public PackageInstallationSpec setPackageContents(byte[] thePackageContents) { + myPackageContents = thePackageContents; + return this; + } + + public PackageInstallationSpec addDependencyExclude(String theExclude) { + getDependencyExcludes().add(theExclude); + return this; + } + + public PackageInstallationSpec addInstallResourceTypes(String... theResourceTypes) { + for (String next : theResourceTypes) { + getInstallResourceTypes().add(next); + } + return this; + } + + public enum InstallModeEnum { + STORE_ONLY, + STORE_AND_INSTALL + } + + public enum ValidationModeEnum { + NOT_AVAILABLE, + AVAILABLE + } + + public static class ExampleSupplier implements Supplier { + + @Override + public PackageInstallationSpec get() { + return new PackageInstallationSpec() + .setName("hl7.fhir.us.core") + .setVersion("3.1.0") + .setInstallMode(InstallModeEnum.STORE_ONLY) + .setFetchDependencies(true); + } + } + + public static class ExampleSupplier2 implements Supplier { + + @Override + public PackageInstallationSpec get() { + return new PackageInstallationSpec() + .setName("com.example.my-resources") + .setVersion("1.0") + .setPackageUrl("classpath:/my-resources.tgz") + .setInstallMode(InstallModeEnum.STORE_AND_INSTALL) + .addInstallResourceTypes("Organization", "Medication", "PlanDefinition", "SearchParameter"); + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IgInstallerSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java similarity index 70% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IgInstallerSvc.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java index 0dd54500272..5c00c9faaf7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/IgInstallerSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.packages; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; @@ -31,48 +32,60 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.util.FhirTerser; +import com.google.common.collect.Lists; import com.google.gson.Gson; +import com.google.gson.JsonElement; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.utilities.cache.IPackageCacheManager; import org.hl7.fhir.utilities.cache.NpmPackage; -import org.hl7.fhir.utilities.cache.PackageCacheManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PostConstruct; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; -public class IgInstallerSvc { +/** + * @since 5.1.0 + */ +public class PackageInstallerSvcImpl implements IPackageInstallerSvc { - private static final Logger ourLog = LoggerFactory.getLogger(IgInstallerSvc.class); + private static final Logger ourLog = LoggerFactory.getLogger(PackageInstallerSvcImpl.class); + public static List DEFAULT_INSTALL_TYPES = Collections.unmodifiableList(Lists.newArrayList( + "NamingSystem", + "CodeSystem", + "ValueSet", + "StructureDefinition", + "ConceptMap", + "SearchParameter", + "Subscription" + )); boolean enabled = true; - @Autowired private FhirContext fhirContext; @Autowired private DaoRegistry daoRegistry; @Autowired private IValidationSupport validationSupport; + @Autowired + private IHapiPackageCacheManager packageCacheManager; - private PackageCacheManager packageCacheManager; - - private String[] SUPPORTED_RESOURCE_TYPES = new String[] - { "NamingSystem", - "CodeSystem", - "ValueSet", - "StructureDefinition", - "ConceptMap", - "SearchParameter", - "Subscription" }; + /** + * Constructor + */ + public PackageInstallerSvcImpl() { + super(); + } @PostConstruct public void initialize() { @@ -90,91 +103,76 @@ public class IgInstallerSvc { enabled = false; } } - try { - packageCacheManager = new PackageCacheManager(true, 1); - } catch (IOException e) { - ourLog.error("Unable to initialize PackageCacheManager: {}", e); - enabled = false; - } - } - - /** - * Loads and installs an IG tarball (with its dependencies) from the specified url. - * - * Installs the IG by persisting instances of the following types of resources: - * - * - NamingSystem, CodeSystem, ValueSet, StructureDefinition (with snapshots), - * ConceptMap, SearchParameter, Subscription - * - * Creates the resources if non-existent, updates them otherwise. - * - * @param url of IG tarball - * @throws ImplementationGuideInstallationException if installation fails - */ - public void install(String url) throws ImplementationGuideInstallationException { - if (enabled) { - try { - install(NpmPackage.fromPackage(toInputStream(url))); - } catch (IOException e) { - ourLog.error("Could not load implementation guide from URL {}", url, e); - } - } - } - - private InputStream toInputStream(String url) throws IOException { - URL u = new URL(url); - URLConnection c = u.openConnection(); - return c.getInputStream(); } /** * Loads and installs an IG from a file on disk or the Simplifier repo using - * the {@link PackageCacheManager}. - * + * the {@link IPackageCacheManager}. + *

* Installs the IG by persisting instances of the following types of resources: - * + *

* - NamingSystem, CodeSystem, ValueSet, StructureDefinition (with snapshots), - * ConceptMap, SearchParameter, Subscription - * + * ConceptMap, SearchParameter, Subscription + *

* Creates the resources if non-existent, updates them otherwise. * - * @param id of the package, or name of folder in filesystem - * @param version of package, or path to folder in filesystem - * @throws ImplementationGuideInstallationException if installation fails + * @param theInstallationSpec The details about what should be installed */ - public void install(String id, String version) throws ImplementationGuideInstallationException { + @Override + public PackageInstallOutcomeJson install(PackageInstallationSpec theInstallationSpec) throws ImplementationGuideInstallationException { + PackageInstallOutcomeJson retVal = new PackageInstallOutcomeJson(); if (enabled) { try { - install(packageCacheManager.loadPackage(id, version)); + NpmPackage npmPackage = packageCacheManager.installPackage(theInstallationSpec); + if (npmPackage == null) { + throw new IOException("Package not found"); + } + + retVal.getMessage().addAll(JpaPackageCache.getProcessingMessages(npmPackage)); + + if (theInstallationSpec.isFetchDependencies()) { + fetchAndInstallDependencies(npmPackage, theInstallationSpec, retVal); + } + + if (theInstallationSpec.getInstallMode() == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) { + install(npmPackage, theInstallationSpec); + } + } catch (IOException e) { - ourLog.error("Could not load implementation guide from packages.fhir.org or " + - "file on disk using ID {} and version {}", id, version, e); + throw new ImplementationGuideInstallationException("Could not load NPM package " + theInstallationSpec.getName() + "#" + theInstallationSpec.getVersion(), e); } } + + return retVal; } /** * Installs a package and its dependencies. - * + *

* Fails fast if one of its dependencies could not be installed. * * @throws ImplementationGuideInstallationException if installation fails */ - private void install(NpmPackage npmPackage) throws ImplementationGuideInstallationException { + private void install(NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec) throws ImplementationGuideInstallationException { String name = npmPackage.getNpm().get("name").getAsString(); String version = npmPackage.getNpm().get("version").getAsString(); + String fhirVersion = npmPackage.fhirVersion(); String currentFhirVersion = fhirContext.getVersion().getVersion().getFhirVersionString(); - assertFhirVersionsAreCompatible(fhirVersion, currentFhirVersion); - fetchAndInstallDependencies(npmPackage); + List installTypes; + if (!theInstallationSpec.getInstallResourceTypes().isEmpty()) { + installTypes = theInstallationSpec.getInstallResourceTypes(); + } else { + installTypes = DEFAULT_INSTALL_TYPES; + } ourLog.info("Installing package: {}#{}", name, version); - int[] count = new int[SUPPORTED_RESOURCE_TYPES.length]; + int[] count = new int[installTypes.size()]; - for (int i = 0; i < SUPPORTED_RESOURCE_TYPES.length; i++) { - Collection resources = parseResourcesOfType(SUPPORTED_RESOURCE_TYPES[i], npmPackage); + for (int i = 0; i < installTypes.size(); i++) { + Collection resources = parseResourcesOfType(installTypes.get(i), npmPackage); count[i] = resources.size(); try { @@ -182,37 +180,48 @@ public class IgInstallerSvc { .map(r -> isStructureDefinitionWithoutSnapshot(r) ? generateSnapshot(r) : r) .forEach(r -> createOrUpdate(r)); } catch (Exception e) { - throw new ImplementationGuideInstallationException(String.format( - "Error installing IG %s#%s: ", name, version), e); + throw new ImplementationGuideInstallationException(String.format("Error installing IG %s#%s: %s", name, version, e.toString()), e); } } ourLog.info(String.format("Finished installation of package %s#%s:", name, version)); for (int i = 0; i < count.length; i++) { - ourLog.info(String.format("-- Created or updated %s resources of type %s", count[i], SUPPORTED_RESOURCE_TYPES[i])); + ourLog.info(String.format("-- Created or updated %s resources of type %s", count[i], installTypes.get(i))); } } - private void fetchAndInstallDependencies(NpmPackage npmPackage) throws ImplementationGuideInstallationException { + private void fetchAndInstallDependencies(NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome) throws ImplementationGuideInstallationException { if (npmPackage.getNpm().has("dependencies")) { - Map dependencies = new Gson().fromJson(npmPackage.getNpm().get("dependencies"), HashMap.class); + JsonElement dependenciesElement = npmPackage.getNpm().get("dependencies"); + Map dependencies = new Gson().fromJson(dependenciesElement, HashMap.class); for (Map.Entry d : dependencies.entrySet()) { String id = d.getKey(); String ver = d.getValue(); - if (id.startsWith("hl7.fhir")) { - continue; // todo : which packages to ignore? - } - if (packageCacheManager == null) { - throw new ImplementationGuideInstallationException(String.format( - "Cannot install dependency %s#%s due to PacketCacheManager initialization error", id, ver)); - } try { + theOutcome.getMessage().add("Package " + npmPackage.id() + "#" + npmPackage.version() + " depends on package " + id + "#" + ver); + + boolean skip = false; + for (String next : theInstallationSpec.getDependencyExcludes()) { + if (id.matches(next)) { + theOutcome.getMessage().add("Not installing dependency " + id + " because it matches exclude criteria: " + next); + skip = true; + break; + } + } + if (skip) { + continue; + } + // resolve in local cache or on packages.fhir.org NpmPackage dependency = packageCacheManager.loadPackage(id, ver); // recursive call to install dependencies of a package before // installing the package - fetchAndInstallDependencies(dependency); - install(dependency); + fetchAndInstallDependencies(dependency, theInstallationSpec, theOutcome); + + if (theInstallationSpec.getInstallMode() == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) { + install(dependency, theInstallationSpec); + } + } catch (IOException e) { throw new ImplementationGuideInstallationException(String.format( "Cannot resolve dependency %s#%s", id, ver), e); @@ -228,8 +237,11 @@ public class IgInstallerSvc { private void assertFhirVersionsAreCompatible(String fhirVersion, String currentFhirVersion) throws ImplementationGuideInstallationException { - boolean compatible = fhirVersion.charAt(0) == currentFhirVersion.charAt(0) && - currentFhirVersion.compareTo(fhirVersion) >= 0; + FhirVersionEnum fhirVersionEnum = FhirVersionEnum.forVersionString(fhirVersion); + FhirVersionEnum currentFhirVersionEnum = FhirVersionEnum.forVersionString(currentFhirVersion); + Validate.notNull(fhirVersionEnum, "Invalid FHIR version string: %s", fhirVersion); + Validate.notNull(currentFhirVersionEnum, "Invalid FHIR version string: %s", currentFhirVersion); + boolean compatible = fhirVersionEnum.equals(currentFhirVersionEnum); if (!compatible) { throw new ImplementationGuideInstallationException(String.format( "Cannot install implementation guide: FHIR versions mismatch (expected <=%s, package uses %s)", @@ -292,7 +304,6 @@ public class IgInstallerSvc { } private SearchParameterMap createSearchParameterMapFor(IBaseResource resource) { - FhirTerser terser = fhirContext.newTerser(); if (resource.getClass().getSimpleName().equals("NamingSystem")) { String uniqueId = extractUniqeIdFromNamingSystem(resource); return new SearchParameterMap().add("value", new StringParam(uniqueId).setExact(true)); @@ -332,16 +343,6 @@ public class IgInstallerSvc { } } - private static IBaseResource getFirstResourceFrom(IBundleProvider searchResult) { - try { - return searchResult.getResources(0, 0).get(0); - } catch (IndexOutOfBoundsException e) { - ourLog.warn("Error when extracting resource from search result " + - "(search result should have been non-empty))", e); - return null; - } - } - private String extractUniqeIdFromNamingSystem(IBaseResource resource) { FhirTerser terser = fhirContext.newTerser(); IBase uniqueIdComponent = (IBase) terser.getSingleValueOrNull(resource, "uniqueId"); @@ -363,4 +364,14 @@ public class IgInstallerSvc { IPrimitiveType asPrimitiveType = (IPrimitiveType) terser.getSingleValueOrNull(resource, "url"); return (String) asPrimitiveType.getValue(); } + + private static IBaseResource getFirstResourceFrom(IBundleProvider searchResult) { + try { + return searchResult.getResources(0, 0).get(0); + } catch (IndexOutOfBoundsException e) { + ourLog.warn("Error when extracting resource from search result " + + "(search result should have been non-empty))", e); + return null; + } + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageSearchSpec.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageSearchSpec.java new file mode 100644 index 00000000000..02ab264d449 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageSearchSpec.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.apache.commons.lang3.Validate; + +public class PackageSearchSpec { + private int myStart; + private int mySize = 50; + private String myResourceUrl; + private CharSequence myDescription; + private String myFhirVersion; + + public String getFhirVersion() { + return myFhirVersion; + } + + public void setFhirVersion(String theFhirVersion) { + myFhirVersion = theFhirVersion; + } + + public int getSize() { + return mySize; + } + + public void setSize(int theSize) { + Validate.inclusiveBetween(1, 250, theSize, "Number must be between 1-250"); + mySize = theSize; + } + + public int getStart() { + return myStart; + } + + public void setStart(int theStart) { + Validate.inclusiveBetween(0, Integer.MAX_VALUE, theStart, "Number must not be negative"); + myStart = theStart; + } + + public String getResourceUrl() { + return myResourceUrl; + } + + public void setResourceUrl(String theResourceUrl) { + myResourceUrl = theResourceUrl; + } + + public CharSequence getDescription() { + return myDescription; + } + + public void setDescription(CharSequence theDescription) { + myDescription = theDescription; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageVersionComparator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageVersionComparator.java new file mode 100644 index 00000000000..ceeb33ac33f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageVersionComparator.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.jpa.packages; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.math.BigDecimal; +import java.util.Comparator; + +import static org.apache.commons.lang3.StringUtils.isNumeric; + +public class PackageVersionComparator implements Comparator { + public static final PackageVersionComparator INSTANCE = new PackageVersionComparator(); + + @Override + public int compare(String o1, String o2) { + + String[] o1parts = o1.split("\\."); + String[] o2parts = o2.split("\\."); + + for (int i = 0; i < o1parts.length && i < o2parts.length; i++) { + String i1part = o1parts[i]; + String i2part = o2parts[i]; + + if (isNumeric(i1part)) { + if (isNumeric(i2part)) { + int cmp = new BigDecimal(i1part).compareTo(new BigDecimal(i2part)); + if (cmp != 0) { + return cmp; + } + } + } + + int cmp = i1part.compareTo(i2part); + if (cmp != 0) { + return cmp; + } + } + + return o1parts.length - o2parts.length; + } + + public static boolean isEquivalent(String theSpec, String thePackageVersion) { + String[] o1parts = theSpec.split("\\."); + String[] o2parts = thePackageVersion.split("\\."); + + for (int i = 0; i < o1parts.length; i++ ) { + if (!o1parts[i].equals("x")) { + if (!o1parts[i].equals(o2parts[i])) { + return false; + } + } + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/patch/XmlPatchUtils.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/patch/XmlPatchUtils.java index b8ae5b28d61..6b030f97d22 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/patch/XmlPatchUtils.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/patch/XmlPatchUtils.java @@ -10,6 +10,8 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import static ca.uhn.fhir.util.StringUtil.toUtf8String; + /* * #%L * HAPI FHIR JPA Server @@ -46,7 +48,7 @@ public class XmlPatchUtils { throw new InternalErrorException(e); } - String resultString = new String(result.toByteArray(), Constants.CHARSET_UTF8); + String resultString = toUtf8String(result.toByteArray()); T retVal = theCtx.newXmlParser().parseResource(clazz, resultString); return retVal; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java index c4bd44851a9..19544975083 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; @@ -173,8 +174,11 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { public static final int DEFAULT_FETCH_SIZE = 250; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseTermReadSvcImpl.class); private static final ValueSetExpansionOptions DEFAULT_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); + private static final TermCodeSystemVersion NO_CURRENT_VERSION = new TermCodeSystemVersion().setId(-1L); private static boolean ourLastResultsFromTranslationCache; // For testing. private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing. + private final int myFetchSize = DEFAULT_FETCH_SIZE; + private final Cache myCodeSystemCurrentVersionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(); @Autowired protected DaoRegistry myDaoRegistry; @Autowired @@ -209,7 +213,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { private DaoConfig myDaoConfig; private Cache> myTranslationCache; private Cache> myTranslationWithReverseCache; - private final int myFetchSize = DEFAULT_FETCH_SIZE; private TransactionTemplate myTxTemplate; @Autowired private PlatformTransactionManager myTransactionManager; @@ -227,12 +230,15 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { private ITermCodeSystemStorageSvc myConceptStorageSvc; @Autowired private ApplicationContext myApplicationContext; + @Autowired + private DefaultProfileValidationSupport myDefaultProfileValidationSupport; private volatile IValidationSupport myJpaValidationSupport; private volatile IValidationSupport myValidationSupport; @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { - return supportsSystem(theSystem); + TermCodeSystemVersion cs = getCurrentCodeSystemVersion(theSystem); + return cs != null; } private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter) { @@ -283,16 +289,10 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { * This method is present only for unit tests, do not call from client code */ @VisibleForTesting - public void clearTranslationCache() { + public void clearCaches() { myTranslationCache.invalidateAll(); - } - - /** - * This method is present only for unit tests, do not call from client code - */ - @VisibleForTesting() - public void clearTranslationWithReverseCache() { myTranslationWithReverseCache.invalidateAll(); + myCodeSystemCurrentVersionCache.invalidateAll(); } public void deleteConceptMap(ResourceTable theResourceTable) { @@ -1289,15 +1289,35 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY); return txTemplate.execute(t -> { - TermCodeSystemVersion csv = null; - TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theCodeSystem); - if (cs != null && cs.getCurrentVersion() != null) { - csv = cs.getCurrentVersion(); + TermCodeSystemVersion csv = getCurrentCodeSystemVersion(theCodeSystem); + if (csv == null) { + return null; } return myConceptDao.findByCodeSystemAndCode(csv, theCode); }); } + @Nullable + private TermCodeSystemVersion getCurrentCodeSystemVersion(String theUri) { + TermCodeSystemVersion retVal = myCodeSystemCurrentVersionCache.get(theUri, uri -> { + TermCodeSystemVersion csv = null; + TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(uri); + if (cs != null && cs.getCurrentVersion() != null) { + csv = cs.getCurrentVersion(); + } + if (csv != null) { + return csv; + } else { + return NO_CURRENT_VERSION; + } + }); + if (retVal == NO_CURRENT_VERSION) { + return null; + } + return retVal; + } + + @Transactional(propagation = Propagation.REQUIRED) @Override public Set findCodesAbove(Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) { @@ -1715,12 +1735,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return null; } - @Override - public boolean supportsSystem(String theSystem) { - TermCodeSystem cs = getCodeSystem(theSystem); - return cs != null; - } - private ArrayList toVersionIndependentConcepts(String theSystem, Set codes) { ArrayList retVal = new ArrayList<>(codes.size()); for (TermConcept next : codes) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java index 7b4fdb35225..9411117e80b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/api/ITermReadSvc.java @@ -94,8 +94,6 @@ public interface ITermReadSvc extends IValidationSupport { void storeTermValueSet(ResourceTable theResourceTable, ValueSet theValueSet); - boolean supportsSystem(String theCodeSystem); - List translate(TranslationRequest theTranslationRequest); List translateWithReverse(TranslationRequest theTranslationRequest); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java index 1f53a02cb23..4e6e2b6b36f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java @@ -180,18 +180,21 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe /** * Log all captured UPDATE queries */ - public void logUpdateQueriesForCurrentThread() { + public String logUpdateQueriesForCurrentThread() { List queries = getUpdateQueriesForCurrentThread() .stream() .map(CircularQueueCaptureQueriesListener::formatQueryAsSql) .collect(Collectors.toList()); - ourLog.info("Update Queries:\n{}", String.join("\n", queries)); + String joined = String.join("\n", queries); + ourLog.info("Update Queries:\n{}", joined); + return joined; } /** * Log all captured SELECT queries + * @return */ - public void logSelectQueriesForCurrentThread(int... theIndexes) { + public String logSelectQueriesForCurrentThread(int... theIndexes) { List queries = getSelectQueriesForCurrentThread() .stream() .map(CircularQueueCaptureQueriesListener::formatQueryAsSql) @@ -205,7 +208,9 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe queries = newList; } - ourLog.info("Select Queries:\n{}", String.join("\n", queries)); + String queriesAsString = String.join("\n", queries); + ourLog.info("Select Queries:\n{}", queriesAsString); + return queriesAsString; } /** @@ -234,12 +239,14 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe /** * Log all captured INSERT queries */ - public void logInsertQueriesForCurrentThread() { + public String logInsertQueriesForCurrentThread() { List queries = getInsertQueriesForCurrentThread() .stream() .map(CircularQueueCaptureQueriesListener::formatQueryAsSql) .collect(Collectors.toList()); - ourLog.info("Insert Queries:\n{}", String.join("\n", queries)); + String queriesAsString = String.join("\n", queries); + ourLog.info("Insert Queries:\n{}", queriesAsString); + return queriesAsString; } /** @@ -278,14 +285,17 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe /** * Log all captured DELETE queries */ - public void logDeleteQueriesForCurrentThread() { + public String logDeleteQueriesForCurrentThread() { List queries = getDeleteQueriesForCurrentThread() .stream() .map(CircularQueueCaptureQueriesListener::formatQueryAsSql) .collect(Collectors.toList()); - ourLog.info("Delete Queries:\n{}", String.join("\n", queries)); + String joined = String.join("\n", queries); + ourLog.info("Delete Queries:\n{}", joined); + return joined; } + /** * Log all captured DELETE queries */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java index 49ffcacb283..c285020d284 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; @@ -46,6 +47,8 @@ public class JpaValidationSupportChain extends ValidationSupportChain { private IValidationSupport myDefaultProfileValidationSupport; @Autowired private ITermReadSvc myTerminologyService; + @Autowired + private NpmJpaValidationSupport myNpmJpaValidationSupport; public JpaValidationSupportChain(FhirContext theFhirContext) { myFhirContext = theFhirContext; @@ -63,12 +66,13 @@ public class JpaValidationSupportChain extends ValidationSupportChain { @PostConstruct public void postConstruct() { - addValidationSupport((IValidationSupport) new CommonCodeSystemsTerminologyService(myFhirContext)); + addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext)); addValidationSupport(myDefaultProfileValidationSupport); addValidationSupport(myJpaValidationSupport); - addValidationSupport((IValidationSupport) myTerminologyService); - addValidationSupport((IValidationSupport) new SnapshotGeneratingValidationSupport(myFhirContext)); - addValidationSupport((IValidationSupport) new InMemoryTerminologyServerValidationSupport(myFhirContext)); + addValidationSupport(myTerminologyService); + addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext)); + addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext)); + addValidationSupport(myNpmJpaValidationSupport); } } diff --git a/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/NHSD.Assets.STU3.tar.gz b/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/NHSD.Assets.STU3.tar.gz deleted file mode 100644 index b6e5628f0f1..00000000000 Binary files a/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/NHSD.Assets.STU3.tar.gz and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java index 92dd30de97b..ee52d38d72e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.validation.IInstanceValidatorModule; import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; @@ -152,12 +153,12 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { */ @Bean @Lazy - public RequestValidatingInterceptor requestValidatingInterceptor() { + public RequestValidatingInterceptor requestValidatingInterceptor(IInstanceValidatorModule theInstanceValidator) { RequestValidatingInterceptor requestValidator = new RequestValidatingInterceptor(); requestValidator.setFailOnSeverity(ResultSeverityEnum.ERROR); requestValidator.setAddResponseHeaderOnSeverity(null); requestValidator.setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum.INFORMATION); - requestValidator.addValidatorModule(instanceValidator()); + requestValidator.addValidatorModule(theInstanceValidator); return requestValidator; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index adc04f66ce1..a03f76c097f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.executor.InterceptorService; @@ -8,6 +9,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermConcept; @@ -30,6 +32,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.test.BaseTest; import ca.uhn.fhir.test.utilities.LoggingRule; +import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.test.utilities.UnregisterScheduledProcessor; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.StopWatch; @@ -54,6 +57,8 @@ import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.PlatformTransactionManager; @@ -125,6 +130,10 @@ public abstract class BaseJpaTest extends BaseTest { private IdHelperService myIdHelperService; @Autowired private MemoryCacheService myMemoryCacheService; + @Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT) + @Autowired + private IValidationSupport myJpaPersistedValidationSupport; + @After public void afterPerformCleanup() { @@ -138,6 +147,11 @@ public abstract class BaseJpaTest extends BaseTest { if (myMemoryCacheService != null) { myMemoryCacheService.invalidateAllCaches(); } + if (myJpaPersistedValidationSupport != null) { + ProxyUtil.getSingletonTarget(myJpaPersistedValidationSupport, JpaPersistedResourceValidationSupport.class).clearCaches(); + } + + } @After @@ -423,11 +437,15 @@ public abstract class BaseJpaTest extends BaseTest { } public static String loadClasspath(String resource) throws IOException { + return new String(loadClasspathBytes(resource), Constants.CHARSET_UTF8); + } + + public static byte[] loadClasspathBytes(String resource) throws IOException { InputStream bundleRes = SystemProviderDstu2Test.class.getResourceAsStream(resource); if (bundleRes == null) { throw new NullPointerException("Can not load " + resource); } - return IOUtils.toString(bundleRes, Constants.CHARSET_UTF8); + return IOUtils.toByteArray(bundleRes); } protected static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry, IBulkDataExportSvc theBulkDataExportSvc) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 046bbdcd2c5..02b8b6b8dce 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -121,6 +121,7 @@ import org.springframework.transaction.support.TransactionTemplate; import javax.persistence.EntityManager; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Map; import static org.hl7.fhir.convertors.conv30_40.ConceptMap30_40.convertConceptMap; @@ -356,8 +357,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @After public void afterClearTerminologyCaches() { BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc); - baseHapiTerminologySvc.clearTranslationCache(); - baseHapiTerminologySvc.clearTranslationWithReverseCache(); + baseHapiTerminologySvc.clearCaches(); BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache(); BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache(); TermDeferredStorageSvcImpl deferredSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc); @@ -412,7 +412,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { if (stream == null) { fail("Unable to load resource: " + resourceName); } - String string = IOUtils.toString(stream, "UTF-8"); + String string = IOUtils.toString(stream, StandardCharsets.UTF_8); IParser newJsonParser = EncodingEnum.detectEncodingNoDefault(string).newParser(myFhirCtx); return newJsonParser.parseResource(type, string); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index 84a06e342ea..f94807291b3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -1,5 +1,8 @@ package ca.uhn.fhir.jpa.dao.dstu3; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.config.BaseConfig; +import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport; import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.MethodOutcome; @@ -7,6 +10,7 @@ import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.io.IOUtils; @@ -23,6 +27,7 @@ import org.junit.AfterClass; import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.util.AopTestUtils; import java.io.IOException; @@ -40,6 +45,9 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { private CachingValidationSupport myValidationSupport; @Autowired private FhirInstanceValidator myFhirInstanceValidator; + @Autowired + @Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT) + private IValidationSupport myPersistedResourceValidationSupport; @Test public void testValidateChangedQuestionnaire() { @@ -78,6 +86,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { ourLog.info("Clearing cache"); myValidationSupport.invalidateCaches(); myFhirInstanceValidator.invalidateCaches(); + ProxyUtil.getSingletonTarget(myPersistedResourceValidationSupport, JpaPersistedResourceValidationSupport.class).clearCaches(); try { myQuestionnaireResponseDao.validate(qr, null, null, null, null, null, null); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index e64d97ccb4c..b55634fc169 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -484,8 +484,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBui @After public void afterClearTerminologyCaches() { BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc); - baseHapiTerminologySvc.clearTranslationCache(); - baseHapiTerminologySvc.clearTranslationWithReverseCache(); + baseHapiTerminologySvc.clearCaches(); BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache(); BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache(); TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 894e7702130..e16923be4f3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -9,11 +9,16 @@ import ca.uhn.fhir.rest.param.ReferenceParam; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Narrative; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.ServiceRequest; +import org.hl7.fhir.r4.model.StringType; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -25,7 +30,6 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.contains; public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class); @@ -114,6 +118,52 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); } + + @Test + public void testValidate() { + + CodeSystem cs = new CodeSystem(); + cs.setUrl("http://foo/cs"); + cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + cs.addConcept().setCode("bar-1").setDisplay("Bar 1"); + cs.addConcept().setCode("bar-2").setDisplay("Bar 2"); + myCodeSystemDao.create(cs); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(cs)); + + Observation obs = new Observation(); +// obs.getMeta().addProfile("http://example.com/fhir/StructureDefinition/vitalsigns-2"); + obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED).setDivAsString("

Hello
"); + obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs"); + obs.setSubject(new Reference("Patient/123")); + obs.addPerformer(new Reference("Practitioner/123")); + obs.setEffective(DateTimeType.now()); + obs.setStatus(Observation.ObservationStatus.FINAL); + obs.setValue(new StringType("This is the value")); + obs.getCode().addCoding().setSystem("http://foo/cs").setCode("bar-1"); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs)); + + // Validate once + myCaptureQueriesListener.clear(); + myObservationDao.validate(obs, null, null, null, null, null, null); + assertEquals(myCaptureQueriesListener.logSelectQueriesForCurrentThread(), 10, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + assertEquals(myCaptureQueriesListener.logUpdateQueriesForCurrentThread(), 0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + assertEquals(myCaptureQueriesListener.logInsertQueriesForCurrentThread(), 0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + assertEquals(myCaptureQueriesListener.logDeleteQueriesForCurrentThread(), 0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + + // Validate again (should rely only on caches) + myCaptureQueriesListener.clear(); + myObservationDao.validate(obs, null, null, null, null, null, null); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); + myCaptureQueriesListener.logInsertQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); + myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); + } + + @Test public void testVRead() { IIdType id = runInTransaction(() -> { @@ -488,12 +538,10 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { } - - @Test public void testTransactionWithMultipleReferences() { Bundle input = new Bundle(); - + Patient patient = new Patient(); patient.setId(IdType.newRandomUuid()); patient.setActive(true); @@ -1079,8 +1127,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); mySystemDao.transaction(mySrd, input); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); - assertEquals(8, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); + assertEquals(myCaptureQueriesListener.logSelectQueriesForCurrentThread(), 3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(myCaptureQueriesListener.logInsertQueriesForCurrentThread(), 8, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index fd619b3f736..f143c920ad4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -136,6 +136,27 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { oo = validateAndReturnOutcome(obs); assertEquals(encode(oo), "Unknown code 'http://terminology.hl7.org/CodeSystem/observation-category#FOO'", oo.getIssueFirstRep().getDiagnostics()); + // Make sure we're caching the validations as opposed to hitting the DB every time + myCaptureQueriesListener.clear(); + obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); + obs.getCode().getCoding().clear(); + obs.getCategory().clear(); + obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs"); + obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE4").setDisplay("Display 3"); + oo = validateAndReturnOutcome(obs); + assertEquals(encode(oo), "No issues detected during validation", oo.getIssueFirstRep().getDiagnostics()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + myCaptureQueriesListener.clear(); + obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); + obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE4").setDisplay("Display 3"); + oo = validateAndReturnOutcome(obs); + assertEquals(encode(oo), "No issues detected during validation", oo.getIssueFirstRep().getDiagnostics()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + + + } /** diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java index 767aa723c89..4894a38c2cf 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java @@ -429,8 +429,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest { @After public void afterClearTerminologyCaches() { BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc); - baseHapiTerminologySvc.clearTranslationCache(); - baseHapiTerminologySvc.clearTranslationWithReverseCache(); + baseHapiTerminologySvc.clearCaches(); BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache(); BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache(); TermDeferredStorageSvcImpl deferredStorageSvc = AopTestUtils.getTargetObject(myTermDeferredStorageSvc); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestDstu3.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestDstu3.java index 4d13c7b4ae4..3924b5d27af 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestDstu3.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestDstu3.java @@ -1,53 +1,215 @@ package ca.uhn.fhir.jpa.packages; import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; -import org.hl7.fhir.utilities.cache.PackageCacheManager; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.ProxyUtil; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.utilities.cache.IPackageCacheManager; +import org.junit.After; +import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import java.io.IOException; import java.io.InputStream; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.util.ClasspathUtil.loadResourceAsByteArray; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class IgInstallerTestDstu3 extends BaseJpaDstu3Test { + private static final Logger ourLog = LoggerFactory.getLogger(IgInstallerTestDstu3.class); @Autowired private DaoConfig daoConfig; @Autowired - private IgInstallerSvc igInstaller; - - @Rule - public final ExpectedException expectedException = ExpectedException.none(); + private PackageInstallerSvcImpl igInstaller; + @Autowired + private IPackageCacheManager myPackageCacheManager; + private Server myServer; + private NpmTestR4.FakeNpmServlet myFakeNpmServlet; + @Autowired + private INpmPackageVersionDao myPackageVersionDao; + private int myPort; @Before - public void before() throws IOException { - PackageCacheManager packageCacheManager = new PackageCacheManager(true, 1); + public void before() throws Exception { + JpaPackageCache jpaPackageCache = ProxyUtil.getSingletonTarget(myPackageCacheManager, JpaPackageCache.class); + + myServer = new Server(0); + ServletHandler proxyHandler = new ServletHandler(); + myFakeNpmServlet = new NpmTestR4.FakeNpmServlet(); + ServletHolder servletHolder = new ServletHolder(myFakeNpmServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + myServer.setHandler(proxyHandler); + myServer.start(); + + myPort = JettyUtil.getPortForStartedServer(myServer); + jpaPackageCache.getPackageServers().clear(); + jpaPackageCache.addPackageServer("http://localhost:" + myPort); + + myFakeNpmServlet.getResponses().clear(); + InputStream stream; - stream = IgInstallerTestDstu3.class.getResourceAsStream("erroneous-ig.tar.gz"); - packageCacheManager.addPackageToCache("erroneous-ig", "1.0.0", stream, "erroneous-ig"); - stream = IgInstallerTestDstu3.class.getResourceAsStream("NHSD.Assets.STU3.tar.gz"); - packageCacheManager.addPackageToCache("NHSD.Assets.STU3", "1.0.0", stream, "NHSD.Assets.STU3"); - stream = IgInstallerTestDstu3.class.getResourceAsStream("basisprofil.de.tar.gz"); - packageCacheManager.addPackageToCache("basisprofil.de", "0.2.40", stream, "basisprofil.de"); } - @Test(expected = ImplementationGuideInstallationException.class) - public void negativeTestInstallFromCache() { - // Unknown base of StructureDefinitions - igInstaller.install("erroneous-ig", "1.0.0"); + @After + public void after() throws Exception { + JettyUtil.closeServer(myServer); + daoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); } @Test - public void installFromCache() { + public void testNegativeInstallFromCache() { daoConfig.setAllowExternalReferences(true); - igInstaller.install("NHSD.Assets.STU3", "1.2.0"); + + byte[] bytes = loadResourceAsByteArray("/packages/erroneous-ig.tar.gz"); + + // Unknown base of StructureDefinitions + try { + igInstaller.install(new PackageInstallationSpec().setName("erroneous-ig").setVersion("1.0.2").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL).setPackageContents(bytes)); + fail(); + } catch (ImplementationGuideInstallationException e) { + Assert.assertThat(e.getMessage(), containsString("Failure when generating snapshot of StructureDefinition")); + } + } + + @Test + public void testInstallPackageWithDependencies() { + byte[] bytes; + + bytes = loadResourceAsByteArray("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); + myFakeNpmServlet.getResponses().put("/nictiz.fhir.nl.stu3.questionnaires/1.0.2", bytes); + + bytes = loadResourceAsByteArray("/packages/nictiz.fhir.nl.stu3.zib2017-1.3.10.tgz"); + myFakeNpmServlet.getResponses().put("/nictiz.fhir.nl.stu3.zib2017/1.3.x", bytes); + + daoConfig.setAllowExternalReferences(true); + PackageInstallationSpec spec = new PackageInstallationSpec() + .setName("nictiz.fhir.nl.stu3.questionnaires") + .setVersion("1.0.2") + .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) + .setFetchDependencies(true) + .addDependencyExclude("hl7\\.fhir\\.[a-zA-Z0-9]+\\.core"); + PackageInstallOutcomeJson outcome = igInstaller.install(spec); + ourLog.info("Install messages:\n * {}", outcome.getMessage().stream().collect(Collectors.joining("\n * "))); + assertThat(outcome.getMessage(), hasItem("Indexing Resource[package/vl-QuestionnaireProvisioningTask.json] with URL: http://nictiz.nl/fhir/StructureDefinition/vl-QuestionnaireProvisioningTask|1.0.1")); + + runInTransaction(() -> { + assertTrue(myPackageVersionDao.findByPackageIdAndVersion("nictiz.fhir.nl.stu3.questionnaires", "1.0.2").isPresent()); + assertTrue(myPackageVersionDao.findByPackageIdAndVersion("nictiz.fhir.nl.stu3.zib2017", "1.3.10").isPresent()); + }); + + } + + @Test + public void testInstallPackageWithoutDependencies() { + byte[] bytes; + + bytes = loadResourceAsByteArray("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); + myFakeNpmServlet.getResponses().put("/nictiz.fhir.nl.stu3.questionnaires/1.0.2", bytes); + + bytes = loadResourceAsByteArray("/packages/nictiz.fhir.nl.stu3.zib2017-1.3.10.tgz"); + myFakeNpmServlet.getResponses().put("/nictiz.fhir.nl.stu3.zib2017/1.3.x", bytes); + + daoConfig.setAllowExternalReferences(true); + igInstaller.install(new PackageInstallationSpec().setName("nictiz.fhir.nl.stu3.questionnaires").setVersion("1.0.2").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL).setFetchDependencies(false)); + + runInTransaction(() -> { + assertTrue(myPackageVersionDao.findByPackageIdAndVersion("nictiz.fhir.nl.stu3.questionnaires", "1.0.2").isPresent()); + assertFalse(myPackageVersionDao.findByPackageIdAndVersion("nictiz.fhir.nl.stu3.zib2017", "1.3.10").isPresent()); + }); + + } + + @Test + public void testInstallPackageByUrl_Http() { + byte[] bytes; + + bytes = loadResourceAsByteArray("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); + myFakeNpmServlet.getResponses().put("/foo.tgz", bytes); + + igInstaller.install(new PackageInstallationSpec() + .setName("nictiz.fhir.nl.stu3.questionnaires") + .setVersion("1.0.2") + .setPackageUrl("http://localhost:" + myPort + "/foo.tgz") + ); + + runInTransaction(() -> { + assertTrue(myPackageVersionDao.findByPackageIdAndVersion("nictiz.fhir.nl.stu3.questionnaires", "1.0.2").isPresent()); + }); + + } + + @Test + public void testInstallPackageByUrl_Classpath() { + byte[] bytes; + + igInstaller.install(new PackageInstallationSpec() + .setName("nictiz.fhir.nl.stu3.questionnaires") + .setVersion("1.0.2") + .setPackageUrl("classpath:/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz") + ); + + runInTransaction(() -> { + assertTrue(myPackageVersionDao.findByPackageIdAndVersion("nictiz.fhir.nl.stu3.questionnaires", "1.0.2").isPresent()); + }); + + } + + @Test + public void testInstallPackageByUrl_WrongPackageId() { + byte[] bytes; + + bytes = loadResourceAsByteArray("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); + myFakeNpmServlet.getResponses().put("/foo.tgz", bytes); + + try { + igInstaller.install(new PackageInstallationSpec() + .setName("blah") + .setVersion("1.0.2") + .setPackageUrl("http://localhost:" + myPort + "/foo.tgz") + ); + fail(); + } catch (InvalidRequestException e) { + assertEquals("", e.getMessage()); + } + + } + + @Test + public void testInstallPackageByUrl_FailingUrl() { + try { + igInstaller.install(new PackageInstallationSpec() + .setName("blah") + .setVersion("1.0.2") + .setPackageUrl("http://localhost:" + myPort + "/foo.tgz") + ); + fail(); + } catch (InvalidRequestException e) { + assertEquals("", e.getMessage()); + } + } @Test public void installFromCache2() { - igInstaller.install("basisprofil.de", "0.2.40"); + byte[] bytes = loadResourceAsByteArray("/packages/basisprofil.de.tar.gz"); + myFakeNpmServlet.getResponses().put("/basisprofil.de/0.2.40", bytes); + + igInstaller.install(new PackageInstallationSpec().setName("basisprofil.de").setVersion("0.2.40").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL)); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestR4.java deleted file mode 100644 index 7b3242b7649..00000000000 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerTestR4.java +++ /dev/null @@ -1,35 +0,0 @@ -package ca.uhn.fhir.jpa.packages; - -import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; -import org.hl7.fhir.utilities.cache.PackageCacheManager; -import org.junit.Before; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.io.IOException; -import java.io.InputStream; - -import static org.junit.Assert.assertTrue; - -public class IgInstallerTestR4 extends BaseJpaR4Test { - - @Autowired - public DaoConfig daoConfig; - @Autowired - public IgInstallerSvc igInstaller; - - @Before - public void before() throws IOException { - PackageCacheManager packageCacheManager = new PackageCacheManager(true, 1); - InputStream stream; - stream = IgInstallerTestDstu3.class.getResourceAsStream("NHSD.Assets.STU3.tar.gz"); - packageCacheManager.addPackageToCache("NHSD.Assets.STU3", "1.0.0", stream, "NHSD.Assets.STU3"); - } - - @Test(expected = ImplementationGuideInstallationException.class) - public void negativeTestInstallFromCacheVersionMismatch() { - daoConfig.setAllowExternalReferences(true); - igInstaller.install("NHSD.Assets.STU3", "1.2.0"); - } -} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java new file mode 100644 index 00000000000..c71958e63bf --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java @@ -0,0 +1,48 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import org.hl7.fhir.utilities.cache.IPackageCacheManager; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; + +public class JpaPackageCacheTest extends BaseJpaR4Test { + + @Autowired + private IPackageCacheManager myPackageCacheManager; + + @Test + public void testSavePackage() throws IOException { + try (InputStream stream = IgInstallerTestDstu3.class.getResourceAsStream("/packages/basisprofil.de.tar.gz")) { + myPackageCacheManager.addPackageToCache("basisprofil.de", "0.2.40", stream, "basisprofil.de"); + } + + NpmPackage pkg; + + pkg = myPackageCacheManager.loadPackage("basisprofil.de", null); + assertEquals("0.2.40", pkg.version()); + + pkg = myPackageCacheManager.loadPackage("basisprofil.de", "0.2.40"); + assertEquals("0.2.40", pkg.version()); + + try { + myPackageCacheManager.loadPackage("basisprofil.de", "99"); + fail(); + } catch (ResourceNotFoundException e) { + assertEquals("Unable to locate package basisprofil.de#99", e.getMessage()); + } + } + + + @Test + public void testSavePackageCorrectFhirVersion() { + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmSearchTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmSearchTestR4.java new file mode 100644 index 00000000000..86117d9c182 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmSearchTestR4.java @@ -0,0 +1,227 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.jpa.dao.data.INpmPackageDao; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.test.utilities.ProxyUtil; +import ca.uhn.fhir.util.JsonUtil; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +public class NpmSearchTestR4 extends BaseJpaR4Test { + + private static final Logger ourLog = LoggerFactory.getLogger(NpmSearchTestR4.class); + @Autowired + public IPackageInstallerSvc igInstaller; + @Autowired + private IHapiPackageCacheManager myPackageCacheManager; + @Autowired + private NpmJpaValidationSupport myNpmJpaValidationSupport; + @Autowired + private INpmPackageDao myPackageDao; + @Autowired + private INpmPackageVersionDao myPackageVersionDao; + @Autowired + private INpmPackageVersionResourceDao myPackageVersionResourceDao; + + @Before + public void before() throws Exception { + JpaPackageCache jpaPackageCache = ProxyUtil.getSingletonTarget(myPackageCacheManager, JpaPackageCache.class); + jpaPackageCache.getPackageServers().clear(); + } + + @Test + public void testSearch() throws IOException { + PackageInstallationSpec spec; + byte[] bytes; + + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + bytes = loadClasspathBytes("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); + spec = new PackageInstallationSpec().setName("nictiz.fhir.nl.stu3.questionnaires").setVersion("1.0.2").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + NpmPackageSearchResultJson search = myPackageCacheManager.search(new PackageSearchSpec()); + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(2, search.getTotal()); + + assertEquals(2, search.getObjects().size()); + assertEquals("hl7.fhir.uv.shorthand", search.getObjects().get(0).getPackage().getName()); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", search.getObjects().get(0).getPackage().getDescription()); + assertEquals("0.12.0", search.getObjects().get(0).getPackage().getVersion()); + assertEquals(3115, search.getObjects().get(0).getPackage().getBytes()); + assertThat(search.getObjects().get(0).getPackage().getFhirVersion().toString(), search.getObjects().get(0).getPackage().getFhirVersion(), Matchers.contains("4.0.1")); + + assertEquals("nictiz.fhir.nl.stu3.questionnaires", search.getObjects().get(1).getPackage().getName()); + assertEquals("Nictiz NL package of FHIR STU3 conformance resources for MedMij information standard Questionnaires. Includes dependency on Zib2017 and SDC.\\n\\nHCIMs: https://zibs.nl/wiki/HCIM_Release_2017(EN)", search.getObjects().get(1).getPackage().getDescription()); + assertEquals("1.0.2", search.getObjects().get(1).getPackage().getVersion()); + assertThat(search.getObjects().get(1).getPackage().getFhirVersion().toString(), search.getObjects().get(0).getPackage().getFhirVersion(), Matchers.contains("4.0.1")); + + } + + + @Test + public void testSearchByResourceUrl() throws IOException { + PackageInstallationSpec spec; + byte[] bytes; + + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + PackageSearchSpec searchSpec; + NpmPackageSearchResultJson search; + + // Matching URL + myCaptureQueriesListener.clear(); + searchSpec = new PackageSearchSpec(); + searchSpec.setResourceUrl("http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + search = myPackageCacheManager.search(searchSpec); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(1, search.getTotal()); + assertEquals(1, search.getObjects().size()); + assertEquals("hl7.fhir.uv.shorthand", search.getObjects().get(0).getPackage().getName()); + assertEquals("0.12.0", search.getObjects().get(0).getPackage().getVersion()); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", search.getObjects().get(0).getPackage().getDescription()); + assertThat(search.getObjects().get(0).getPackage().getFhirVersion(), Matchers.contains("4.0.1")); + + // Non Matching URL + searchSpec = new PackageSearchSpec(); + searchSpec.setResourceUrl("http://foo"); + search = myPackageCacheManager.search(searchSpec); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(0, search.getTotal()); + assertEquals(0, search.getObjects().size()); + + } + + + @Test + public void testSearchByFhirVersion() throws IOException { + PackageInstallationSpec spec; + byte[] bytes; + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + PackageSearchSpec searchSpec; + NpmPackageSearchResultJson search; + + // Matching by name + myCaptureQueriesListener.clear(); + searchSpec = new PackageSearchSpec(); + searchSpec.setFhirVersion("R4"); + search = myPackageCacheManager.search(searchSpec); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(1, search.getTotal()); + assertEquals("hl7.fhir.uv.shorthand", search.getObjects().get(0).getPackage().getName()); + assertEquals("4.0.1", search.getObjects().get(0).getPackage().getFhirVersion().get(0)); + + // Matching FHIR version + myCaptureQueriesListener.clear(); + searchSpec = new PackageSearchSpec(); + searchSpec.setFhirVersion("4.0.1"); + search = myPackageCacheManager.search(searchSpec); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(1, search.getTotal()); + assertEquals("hl7.fhir.uv.shorthand", search.getObjects().get(0).getPackage().getName()); + + // Partial Matching FHIR version + myCaptureQueriesListener.clear(); + searchSpec = new PackageSearchSpec(); + searchSpec.setFhirVersion("4.0"); + search = myPackageCacheManager.search(searchSpec); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(1, search.getTotal()); + assertEquals("hl7.fhir.uv.shorthand", search.getObjects().get(0).getPackage().getName()); + + // Non Matching URL + searchSpec = new PackageSearchSpec(); + searchSpec.setResourceUrl("http://foo"); + search = myPackageCacheManager.search(searchSpec); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(0, search.getTotal()); + assertEquals(0, search.getObjects().size()); + + } + + + @Test + public void testSearchByDescription() throws IOException { + PackageInstallationSpec spec; + byte[] bytes; + + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(bytes); + igInstaller.install(spec); + + PackageSearchSpec searchSpec; + NpmPackageSearchResultJson search; + + // Matching URL + myCaptureQueriesListener.clear(); + searchSpec = new PackageSearchSpec(); + searchSpec.setDescription("shorthand"); + search = myPackageCacheManager.search(searchSpec); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + + runInTransaction(()->{ + ourLog.info("Versions:\n * {}", myPackageVersionDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); + }); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(1, search.getTotal()); + assertEquals(1, search.getObjects().size()); + assertEquals("hl7.fhir.uv.shorthand", search.getObjects().get(0).getPackage().getName()); + assertEquals("0.12.0", search.getObjects().get(0).getPackage().getVersion()); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", search.getObjects().get(0).getPackage().getDescription()); + assertThat(search.getObjects().get(0).getPackage().getFhirVersion(), Matchers.contains("4.0.1")); + + // Non Matching URL + searchSpec = new PackageSearchSpec(); + searchSpec.setResourceUrl("http://foo"); + search = myPackageCacheManager.search(searchSpec); + + ourLog.info("Search rersults:\r{}", JsonUtil.serialize(search)); + assertEquals(0, search.getTotal()); + assertEquals(0, search.getObjects().size()); + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmTestDstu3.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmTestDstu3.java new file mode 100644 index 00000000000..324353d854d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmTestDstu3.java @@ -0,0 +1,128 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.ProxyUtil; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.Condition; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class NpmTestDstu3 extends BaseJpaDstu3Test { + + private static final Logger ourLog = LoggerFactory.getLogger(FakeNpmServlet.class); + @Autowired + public PackageInstallerSvcImpl igInstaller; + @Autowired + private IHapiPackageCacheManager myPackageCacheManager; + @Autowired + private NpmJpaValidationSupport myNpmJpaValidationSupport; + + private Server myServer; + private final Map myResponses = new HashMap<>(); + + @Before + public void before() throws Exception { + JpaPackageCache jpaPackageCache = ProxyUtil.getSingletonTarget(myPackageCacheManager, JpaPackageCache.class); + + myServer = new Server(0); + ServletHandler proxyHandler = new ServletHandler(); + FakeNpmServlet fakeNpmServlet = new FakeNpmServlet(); + ServletHolder servletHolder = new ServletHolder(fakeNpmServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + myServer.setHandler(proxyHandler); + myServer.start(); + + int port = JettyUtil.getPortForStartedServer(myServer); + jpaPackageCache.getPackageServers().clear(); + jpaPackageCache.addPackageServer("http://localhost:" + port); + + myResponses.clear(); + } + + @After + public void after() throws Exception { + JettyUtil.closeServer(myServer); + } + + @Test + public void installDstu3Package() throws Exception { + byte[] bytes = loadClasspathBytes("/packages/basisprofil.de.tar.gz"); + myResponses.put("/basisprofil.de/0.2.40", bytes); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("basisprofil.de").setVersion("0.2.40").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + // Be sure no further communication with the server + JettyUtil.closeServer(myServer); + + StructureDefinition sd = (StructureDefinition) myNpmJpaValidationSupport.fetchStructureDefinition("http://fhir.de/StructureDefinition/condition-de-basis/0.2"); + assertEquals("http://fhir.de/StructureDefinition/condition-de-basis/0.2", sd.getUrl()); + + ValueSet vs = (ValueSet) myNpmJpaValidationSupport.fetchValueSet("http://fhir.de/ValueSet/ifa/pzn"); + assertEquals("http://fhir.de/ValueSet/ifa/pzn", vs.getUrl()); + + CodeSystem cs = (CodeSystem) myNpmJpaValidationSupport.fetchCodeSystem("http://fhir.de/CodeSystem/deuev/anlage-8-laenderkennzeichen"); + assertEquals("http://fhir.de/CodeSystem/deuev/anlage-8-laenderkennzeichen", cs.getUrl()); + + // Try and validate using a profile from the IG + Condition condition = new Condition(); + condition.setClinicalStatus(Condition.ConditionClinicalStatus.RESOLVED); + condition.getMeta().addProfile("http://fhir.de/StructureDefinition/condition-de-basis/0.2"); + try { + myConditionDao.validate(condition, null, null, null, ValidationModeEnum.CREATE, null, mySrd); + fail(); + } catch (PreconditionFailedException e) { + ourLog.info("Fail Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome())); + + OperationOutcome oo = (OperationOutcome) e.getOperationOutcome(); + assertEquals("Profile http://fhir.de/StructureDefinition/condition-de-basis/0.2, Element 'Condition.subject': minimum required = 1, but only found 0", oo.getIssueFirstRep().getDiagnostics()); + } + + } + + + private class FakeNpmServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String requestUrl = req.getRequestURI(); + if (myResponses.containsKey(requestUrl)) { + ourLog.info("Responding to request: {}", requestUrl); + + resp.setStatus(200); + resp.setHeader(Constants.HEADER_CONTENT_TYPE, "application/gzip"); + resp.getOutputStream().write(myResponses.get(requestUrl)); + resp.getOutputStream().close(); + } else { + ourLog.warn("Unknown request: {}", requestUrl); + + resp.sendError(404); + } + + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmTestR4.java new file mode 100644 index 00000000000..46f2f4a70a4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmTestR4.java @@ -0,0 +1,474 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.dao.data.INpmPackageDao; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; +import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; +import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.ProxyUtil; +import ca.uhn.fhir.util.JsonUtil; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ImplementationGuide; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +public class NpmTestR4 extends BaseJpaR4Test { + + private static final Logger ourLog = LoggerFactory.getLogger(NpmTestR4.class); + @Autowired + public IPackageInstallerSvc igInstaller; + @Autowired + private IHapiPackageCacheManager myPackageCacheManager; + @Autowired + private NpmJpaValidationSupport myNpmJpaValidationSupport; + private Server myServer; + @Autowired + private INpmPackageDao myPackageDao; + @Autowired + private INpmPackageVersionDao myPackageVersionDao; + @Autowired + private INpmPackageVersionResourceDao myPackageVersionResourceDao; + private FakeNpmServlet myFakeNpmServlet; + + @Before + public void before() throws Exception { + JpaPackageCache jpaPackageCache = ProxyUtil.getSingletonTarget(myPackageCacheManager, JpaPackageCache.class); + + myServer = new Server(0); + ServletHandler proxyHandler = new ServletHandler(); + myFakeNpmServlet = new FakeNpmServlet(); + ServletHolder servletHolder = new ServletHolder(myFakeNpmServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + myServer.setHandler(proxyHandler); + myServer.start(); + + int port = JettyUtil.getPortForStartedServer(myServer); + jpaPackageCache.getPackageServers().clear(); + jpaPackageCache.addPackageServer("http://localhost:" + port); + + myFakeNpmServlet.myResponses.clear(); + } + + @After + public void after() throws Exception { + JettyUtil.closeServer(myServer); + myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); + } + + @Test + public void testCacheDstu3Package() throws Exception { + byte[] bytes = loadClasspathBytes("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); + myFakeNpmServlet.myResponses.put("/nictiz.fhir.nl.stu3.questionnaires/1.0.2", bytes); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("nictiz.fhir.nl.stu3.questionnaires").setVersion("1.0.2").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + // Be sure no further communication with the server + JettyUtil.closeServer(myServer); + + // Make sure we can fetch the package by ID and Version + NpmPackage pkg = myPackageCacheManager.loadPackage("nictiz.fhir.nl.stu3.questionnaires", "1.0.2"); + assertEquals("Nictiz NL package of FHIR STU3 conformance resources for MedMij information standard Questionnaires. Includes dependency on Zib2017 and SDC.\\n\\nHCIMs: https://zibs.nl/wiki/HCIM_Release_2017(EN)", pkg.description()); + + // Make sure we can fetch the package by ID + pkg = myPackageCacheManager.loadPackage("nictiz.fhir.nl.stu3.questionnaires", null); + assertEquals("1.0.2", pkg.version()); + assertEquals("Nictiz NL package of FHIR STU3 conformance resources for MedMij information standard Questionnaires. Includes dependency on Zib2017 and SDC.\\n\\nHCIMs: https://zibs.nl/wiki/HCIM_Release_2017(EN)", pkg.description()); + + // Fetch resource by URL + FhirContext fhirContext = FhirContext.forDstu3(); + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.DSTU3, "http://nictiz.nl/fhir/StructureDefinition/vl-QuestionnaireResponse"); + assertThat(fhirContext.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://nictiz.nl/fhir/StructureDefinition/vl-QuestionnaireResponse\",\"version\":\"1.0.1\"")); + }); + + // Fetch resource by URL with version + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.DSTU3, "http://nictiz.nl/fhir/StructureDefinition/vl-QuestionnaireResponse|1.0.1"); + assertThat(fhirContext.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://nictiz.nl/fhir/StructureDefinition/vl-QuestionnaireResponse\",\"version\":\"1.0.1\"")); + }); + + // This was saved but is the wrong version of FHIR for this server + assertNull(myNpmJpaValidationSupport.fetchStructureDefinition("http://fhir.de/StructureDefinition/condition-de-basis/0.2")); + } + + @Test + public void testInstallR4Package() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + byte[] bytes = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", bytes); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); + igInstaller.install(spec); + + // Be sure no further communication with the server + JettyUtil.closeServer(myServer); + + // Make sure we can fetch the package by ID and Version + NpmPackage pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", "0.12.0"); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", pkg.description()); + + // Make sure we can fetch the package by ID + pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", null); + assertEquals("0.12.0", pkg.version()); + assertEquals("Describes FHIR Shorthand (FSH), a domain-specific language (DSL) for defining the content of FHIR Implementation Guides (IG). (built Wed, Apr 1, 2020 17:24+0000+00:00)", pkg.description()); + + // Make sure DB rows were saved + runInTransaction(() -> { + NpmPackageEntity pkgEntity = myPackageDao.findByPackageId("hl7.fhir.uv.shorthand").orElseThrow(() -> new IllegalArgumentException()); + assertEquals("hl7.fhir.uv.shorthand", pkgEntity.getPackageId()); + + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals("hl7.fhir.uv.shorthand", versionEntity.getPackageId()); + assertEquals("0.12.0", versionEntity.getVersionId()); + assertEquals(3001, versionEntity.getPackageSizeBytes()); + assertEquals(true, versionEntity.isCurrentVersion()); + assertEquals("hl7.fhir.uv.shorthand", versionEntity.getPackageId()); + assertEquals("4.0.1", versionEntity.getFhirVersionId()); + assertEquals(FhirVersionEnum.R4, versionEntity.getFhirVersion()); + + NpmPackageVersionResourceEntity resource = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand").getContent().get(0); + assertEquals("http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand", resource.getCanonicalUrl()); + assertEquals("0.12.0", resource.getCanonicalVersion()); + assertEquals("ImplementationGuide-hl7.fhir.uv.shorthand.json", resource.getFilename()); + assertEquals("4.0.1", resource.getFhirVersionId()); + assertEquals(FhirVersionEnum.R4, resource.getFhirVersion()); + assertEquals(6155, resource.getResSizeBytes()); + }); + + // Fetch resource by URL + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertThat(myFhirCtx.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand\",\"version\":\"0.12.0\"")); + }); + + // Fetch resource by URL with version + runInTransaction(() -> { + IBaseResource asset = myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand|0.12.0"); + assertThat(myFhirCtx.newJsonParser().encodeResourceToString(asset), containsString("\"url\":\"http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand\",\"version\":\"0.12.0\"")); + }); + + // Search for the installed resource + runInTransaction(() -> { + SearchParameterMap map = SearchParameterMap.newSynchronous(); + map.add(StructureDefinition.SP_URL, new UriParam("http://hl7.org/fhir/uv/shorthand/CodeSystem/shorthand-code-system")); + IBundleProvider outcome = myCodeSystemDao.search(map); + assertEquals(1, outcome.sizeOrThrowNpe()); + }); + } + + + @Test + public void testLoadPackageMetadata() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz")); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + runInTransaction(() -> { + NpmPackageMetadataJson metadata = myPackageCacheManager.loadPackageMetadata("hl7.fhir.uv.shorthand"); + try { + ourLog.info(JsonUtil.serialize(metadata)); + + assertEquals("0.12.0", metadata.getDistTags().getLatest()); + + assertThat(metadata.getVersions().keySet(), contains("0.12.0", "0.11.1")); + + NpmPackageMetadataJson.Version version0120 = metadata.getVersions().get("0.12.0"); + assertEquals(3001, version0120.getBytes()); + + } catch (IOException e) { + throw new InternalErrorException(e); + } + }); + + } + + + @Test + public void testLoadPackageUsingImpreciseId() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz")); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.0.tgz")); + + PackageInstallationSpec spec; + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + PackageInstallOutcomeJson outcome = igInstaller.install(spec); + ourLog.info("Install messages:\n * {}", outcome.getMessage().stream().collect(Collectors.joining("\n * "))); + assertThat(outcome.getMessage(), hasItem("Marking package hl7.fhir.uv.shorthand#0.12.0 as current version")); + assertThat(outcome.getMessage(), hasItem("Indexing Resource[package/CodeSystem-shorthand-code-system.json] with URL: http://hl7.org/fhir/uv/shorthand/CodeSystem/shorthand-code-system|0.12.0")); + + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + outcome = igInstaller.install(spec); + ourLog.info("Install messages:\n * {}", outcome.getMessage().stream().collect(Collectors.joining("\n * "))); + assertThat(outcome.getMessage(), not(hasItem("Marking package hl7.fhir.uv.shorthand#0.11.1 as current version"))); + + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + + NpmPackage pkg; + + pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", "0.11.x"); + assertEquals("0.11.1", pkg.version()); + + pkg = myPackageCacheManager.loadPackage("hl7.fhir.uv.shorthand", "0.12.x"); + assertEquals("0.12.0", pkg.version()); + + } + + @Test + public void testInstallNewerPackageUpdatesLatestVersionFlag() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + byte[] contents0111 = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz"); + byte[] contents0120 = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", contents0111); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", contents0120); + + // Install older version + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + // Older version is current + runInTransaction(() -> { + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.11.1").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(true, versionEntity.isCurrentVersion()); + }); + + // Fetching a resource should return the older version + runInTransaction(() -> { + ImplementationGuide ig = (ImplementationGuide) myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertEquals("0.11.1", ig.getVersion()); + }); + + // Now install newer version + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + // Newer version is current + runInTransaction(() -> { + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.11.1").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(false, versionEntity.isCurrentVersion()); + + versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(true, versionEntity.isCurrentVersion()); + }); + + // Fetching a resource should return the newer version + runInTransaction(() -> { + ImplementationGuide ig = (ImplementationGuide) myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertEquals("0.12.0", ig.getVersion()); + }); + } + + @Test + public void testInstallOlderPackageDoesntUpdateLatestVersionFlag() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz")); + + // Install newer version + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + runInTransaction(() -> { + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(true, versionEntity.isCurrentVersion()); + }); + + // Fetching a resource should return the older version + runInTransaction(() -> { + ImplementationGuide ig = (ImplementationGuide) myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertEquals("0.12.0", ig.getVersion()); + }); + + // Install older version + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + // Newer version is still current + runInTransaction(() -> { + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.11.1").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(false, versionEntity.isCurrentVersion()); + + versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(true, versionEntity.isCurrentVersion()); + }); + + // Fetching a resource should return the newer version + runInTransaction(() -> { + ImplementationGuide ig = (ImplementationGuide) myPackageCacheManager.loadPackageAssetByUrl(FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ImplementationGuide/hl7.fhir.uv.shorthand"); + assertEquals("0.12.0", ig.getVersion()); + }); + } + + @Test + public void testInstallAlreadyExistingIsIgnored() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")); + + // Install + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + runInTransaction(() -> { + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(true, versionEntity.isCurrentVersion()); + }); + + // Install same again + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY); + igInstaller.install(spec); + + runInTransaction(() -> { + NpmPackageVersionEntity versionEntity = myPackageVersionDao.findByPackageIdAndVersion("hl7.fhir.uv.shorthand", "0.12.0").orElseThrow(() -> new IllegalArgumentException()); + assertEquals(true, versionEntity.isCurrentVersion()); + }); + + } + + @Test + public void testLoadContents() throws IOException { + byte[] contents0111 = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz"); + byte[] contents0120 = loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz"); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(contents0111); + igInstaller.install(spec); + spec = new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY).setPackageContents(contents0120); + igInstaller.install(spec); + + + assertArrayEquals(contents0111, myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "0.11.1").getBytes()); + assertArrayEquals(contents0120, myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "0.12.0").getBytes()); + assertArrayEquals(contents0120, myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "latest").getBytes()); + assertEquals("0.11.1", myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "0.11.1").getVersion()); + assertEquals("0.12.0", myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "0.12.0").getVersion()); + assertEquals("0.12.0", myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "latest").getVersion()); + assertEquals(null, myPackageCacheManager.loadPackageContents("hl7.fhir.uv.shorthand", "1.2.3")); + assertEquals(null, myPackageCacheManager.loadPackageContents("foo", "1.2.3")); + } + + + @Test + public void testDeletePackage() throws IOException { + myDaoConfig.setAllowExternalReferences(true); + + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.12.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.1", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.1.tgz")); + myFakeNpmServlet.myResponses.put("/hl7.fhir.uv.shorthand/0.11.0", loadClasspathBytes("/packages/hl7.fhir.uv.shorthand-0.11.0.tgz")); + + igInstaller.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.12.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + igInstaller.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.1").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + igInstaller.install(new PackageInstallationSpec().setName("hl7.fhir.uv.shorthand").setVersion("0.11.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + + runInTransaction(() -> { + Slice versions = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ValueSet/shorthand-instance-tags"); + assertEquals(1, versions.getNumberOfElements()); + NpmPackageVersionResourceEntity resource = versions.getContent().get(0); + assertEquals("0.12.0", resource.getCanonicalVersion()); + }); + + myPackageCacheManager.uninstallPackage("hl7.fhir.uv.shorthand", "0.12.0"); + + runInTransaction(() -> { + Slice versions = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ValueSet/shorthand-instance-tags"); + assertEquals(1, versions.getNumberOfElements()); + NpmPackageVersionResourceEntity resource = versions.getContent().get(0); + assertEquals("0.11.1", resource.getCanonicalVersion()); + }); + + myPackageCacheManager.uninstallPackage("hl7.fhir.uv.shorthand", "0.11.0"); + + runInTransaction(() -> { + Slice versions = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ValueSet/shorthand-instance-tags"); + assertEquals(1, versions.getNumberOfElements()); + NpmPackageVersionResourceEntity resource = versions.getContent().get(0); + assertEquals("0.11.1", resource.getCanonicalVersion()); + }); + + myPackageCacheManager.uninstallPackage("hl7.fhir.uv.shorthand", "0.11.1"); + + runInTransaction(() -> { + Slice versions = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl(Pageable.unpaged(), FhirVersionEnum.R4, "http://hl7.org/fhir/uv/shorthand/ValueSet/shorthand-instance-tags"); + assertEquals(0, versions.getNumberOfElements()); + }); + } + + + static class FakeNpmServlet extends HttpServlet { + + private final Map myResponses = new HashMap<>(); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String requestUrl = req.getRequestURI(); + if (myResponses.containsKey(requestUrl)) { + ourLog.info("Responding to request: {}", requestUrl); + + resp.setStatus(200); + resp.setHeader(Constants.HEADER_CONTENT_TYPE, "application/gzip"); + resp.getOutputStream().write(myResponses.get(requestUrl)); + resp.getOutputStream().close(); + } else { + ourLog.warn("Unknown request: {}", requestUrl); + + resp.sendError(404); + } + + } + + public Map getResponses() { + return myResponses; + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/PackageInstallationSpecTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/PackageInstallationSpecTest.java new file mode 100644 index 00000000000..90fefccf0bc --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/PackageInstallationSpecTest.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.jpa.packages; + +import ca.uhn.fhir.util.JsonUtil; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.*; + +public class PackageInstallationSpecTest { + + @Test + public void testExampleSupplier() throws IOException { + PackageInstallationSpec output = new PackageInstallationSpec.ExampleSupplier().get(); + String json = JsonUtil.serialize(output); + assertThat(json, containsString("\"name\" : \"hl7.fhir.us.core\"")); + + output = new PackageInstallationSpec.ExampleSupplier2().get(); + json = JsonUtil.serialize(output); + assertThat(json, containsString("\"packageUrl\" : \"classpath:/my-resources.tgz\"")); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/PackageVersionComparatorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/PackageVersionComparatorTest.java new file mode 100644 index 00000000000..60b7e926163 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/PackageVersionComparatorTest.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.jpa.packages; + +import org.junit.Test; + +import static com.google.common.collect.Lists.newArrayList; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.thymeleaf.util.ListUtils.sort; + +public class PackageVersionComparatorTest { + + private PackageVersionComparator myCmp = new PackageVersionComparator(); + + @Test + public void testCompareVersion() { + assertThat(sort(newArrayList("10.1", "10.2"), myCmp), contains("10.1", "10.2")); + assertThat(sort(newArrayList("10.2", "10.1"), myCmp), contains("10.1", "10.2")); + assertThat(sort(newArrayList("10.1.2.3", "9.1.2.3"), myCmp), contains("9.1.2.3", "10.1.2.3")); + assertThat(sort(newArrayList("9.1.2.3", "10.1.2.3"), myCmp), contains("9.1.2.3", "10.1.2.3")); + assertThat(sort(newArrayList("9.1.2.3", "9.1"), myCmp), contains("9.1", "9.1.2.3")); + assertThat(sort(newArrayList("9.1", "9.1.2.3"), myCmp), contains("9.1", "9.1.2.3")); + assertThat(sort(newArrayList("A", "1"), myCmp), contains("1", "A")); + assertThat(sort(newArrayList("1", "A"), myCmp), contains("1", "A")); + assertThat(sort(newArrayList("A", "B"), myCmp), contains("A", "B")); + } + + @Test + public void testIsEquivalent() { + assertTrue(PackageVersionComparator.isEquivalent("1.2.x", "1.2.3")); + assertTrue(PackageVersionComparator.isEquivalent("1.2", "1.2.3")); + assertTrue(PackageVersionComparator.isEquivalent("1.2.3", "1.2.3")); + assertFalse(PackageVersionComparator.isEquivalent("1.2.4", "1.2.3")); + assertFalse(PackageVersionComparator.isEquivalent("1.3", "1.2.3")); + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/basisprofil.de.tar.gz b/hapi-fhir-jpaserver-base/src/test/resources/packages/basisprofil.de.tar.gz similarity index 100% rename from hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/basisprofil.de.tar.gz rename to hapi-fhir-jpaserver-base/src/test/resources/packages/basisprofil.de.tar.gz diff --git a/hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/erroneous-ig.tar.gz b/hapi-fhir-jpaserver-base/src/test/resources/packages/erroneous-ig.tar.gz similarity index 100% rename from hapi-fhir-jpaserver-base/src/main/resources/ca/uhn/fhir/jpa/packages/erroneous-ig.tar.gz rename to hapi-fhir-jpaserver-base/src/test/resources/packages/erroneous-ig.tar.gz diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.11.0.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.11.0.tgz new file mode 100644 index 00000000000..ce3a642cf2d Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.11.0.tgz differ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.11.1.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.11.1.tgz new file mode 100644 index 00000000000..f9eeb6b5a64 Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.11.1.tgz differ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.12.0.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.12.0.tgz new file mode 100644 index 00000000000..6ba42ef0f8c Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/packages/hl7.fhir.uv.shorthand-0.12.0.tgz differ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz new file mode 100644 index 00000000000..a4b913de0d6 Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz differ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.zib2017-1.3.10.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.zib2017-1.3.10.tgz new file mode 100644 index 00000000000..248c3a83017 Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.zib2017-1.3.10.tgz differ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.zib2017-1.3.9.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.zib2017-1.3.9.tgz new file mode 100644 index 00000000000..6649dcf3559 Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/packages/nictiz.fhir.nl.stu3.zib2017-1.3.9.tgz differ diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index eae0e28ae63..60101f22b16 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -68,7 +68,60 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { init420(); // 20191015 - 20200217 init430(); // Replaced by 5.0.0 init500(); // 20200218 - 20200513 - init501(); // 20200514 - present + init501(); // 20200514 - 20200515 + init510(); // 20200516 - present + } + + private void init510() { + Builder version = forVersion(VersionEnum.V5_1_0); + + // NPM Packages + version.addIdGenerator("20200610.1", "SEQ_NPM_PACK"); + Builder.BuilderAddTableByColumns pkg = version.addTableByColumns("20200610.2", "NPM_PACKAGE", "PID"); + pkg.addColumn("PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkg.addColumn("PACKAGE_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkg.addColumn("CUR_VERSION_ID").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkg.addColumn("UPDATED_TIME").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP); + pkg.addColumn("PACKAGE_DESC").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkg.addIndex("20200610.3", "IDX_PACK_ID").unique(true).withColumns("PACKAGE_ID"); + + version.addIdGenerator("20200610.4", "SEQ_NPM_PACKVER"); + Builder.BuilderAddTableByColumns pkgVer = version.addTableByColumns("20200610.5", "NPM_PACKAGE_VER", "PID"); + pkgVer.addColumn("PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVer.addColumn("PACKAGE_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVer.addColumn("VERSION_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVer.addColumn("PACKAGE_PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVer.addColumn("BINARY_RES_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVer.addColumn("SAVED_TIME").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP); + pkgVer.addColumn("PKG_DESC").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVer.addColumn("DESC_UPPER").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVer.addColumn("CURRENT_VERSION").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.BOOLEAN); + pkgVer.addColumn("FHIR_VERSION_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 10); + pkgVer.addColumn("FHIR_VERSION").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 10); + pkgVer.addColumn("PACKAGE_SIZE_BYTES").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVer.addColumn("UPDATED_TIME").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP); + pkgVer.addForeignKey("20200610.6", "FK_NPM_PKV_PKG").toColumn("PACKAGE_PID").references("NPM_PACKAGE", "PID"); + pkgVer.addForeignKey("20200610.7", "FK_NPM_PKV_RESID").toColumn("BINARY_RES_ID").references("HFJ_RESOURCE", "RES_ID"); + pkgVer.addIndex("20200610.8", "IDX_PACKVER").unique(true).withColumns("PACKAGE_ID", "VERSION_ID"); + + version.addIdGenerator("20200610.9", "SEQ_NPM_PACKVERRES"); + Builder.BuilderAddTableByColumns pkgVerRes = version.addTableByColumns("20200610.10", "NPM_PACKAGE_VER_RES", "PID"); + pkgVerRes.addColumn("PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVerRes.addColumn("PACKVER_PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVerRes.addColumn("BINARY_RES_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVerRes.addColumn("FILE_DIR").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVerRes.addColumn("FILE_NAME").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVerRes.addColumn("RES_TYPE").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 40); + pkgVerRes.addColumn("CANONICAL_URL").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVerRes.addColumn("CANONICAL_VERSION").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + pkgVerRes.addColumn("FHIR_VERSION_ID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 10); + pkgVerRes.addColumn("FHIR_VERSION").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 10); + pkgVerRes.addColumn("RES_SIZE_BYTES").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + pkgVerRes.addColumn("UPDATED_TIME").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP); + pkgVerRes.addForeignKey("20200610.11", "FK_NPM_PACKVERRES_PACKVER").toColumn("PACKVER_PID").references("NPM_PACKAGE_VER", "PID"); + pkgVerRes.addForeignKey("20200610.12", "FK_NPM_PKVR_RESID").toColumn("BINARY_RES_ID").references("HFJ_RESOURCE", "PID"); + pkgVerRes.addIndex("20200610.13", "IDX_PACKVERRES_URL").unique(false).withColumns("CANONICAL_URL"); + } private void init501() { //20200514 - present @@ -246,7 +299,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { protected void init420() { // 20191015 - 20200217 Builder version = forVersion(VersionEnum.V4_2_0); - // TermValueSetConceptDesignation + // TermValueSetConceptDesignation version.onTable("TRM_VALUESET_C_DESIGNATION").dropIndex("20200202.1", "IDX_VALUESET_C_DSGNTN_VAL").failureAllowed(); Builder.BuilderWithTableName searchTable = version.onTable("HFJ_SEARCH"); searchTable.dropIndex("20200203.1", "IDX_SEARCH_LASTRETURNED"); @@ -446,7 +499,6 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .renameColumn("20190722.29", "CODE", "CODEVAL", false, true); - // TermValueSet version.startSectionWithMessage("Processing table: TRM_VALUESET"); version.addIdGenerator("20190722.30", "SEQ_VALUESET_PID"); @@ -749,9 +801,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); spidxString - .addIndex("20180903.27","IDX_SP_STRING_HASH_EXCT") + .addIndex("20180903.27", "IDX_SP_STRING_HASH_EXCT") .unique(false) - .withColumns( "HASH_EXACT"); + .withColumns("HASH_EXACT"); spidxString .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.28") .setColumnName("HASH_NORM_PREFIX") @@ -789,17 +841,17 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .unique(false) .withColumns("HASH_IDENTITY"); spidxToken - .addIndex("20180903.36","IDX_SP_TOKEN_HASH_S") + .addIndex("20180903.36", "IDX_SP_TOKEN_HASH_S") .unique(false) - .withColumns( "HASH_SYS"); + .withColumns("HASH_SYS"); spidxToken .addIndex("20180903.37", "IDX_SP_TOKEN_HASH_SV") .unique(false) .withColumns("HASH_SYS_AND_VALUE"); spidxToken - .addIndex("20180903.38","IDX_SP_TOKEN_HASH_V") + .addIndex("20180903.38", "IDX_SP_TOKEN_HASH_V") .unique(false) - .withColumns( "HASH_VALUE"); + .withColumns("HASH_VALUE"); spidxToken .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.39") .setColumnName("HASH_IDENTITY") @@ -827,9 +879,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); spidxUri - .addIndex("20180903.43","IDX_SP_URI_HASH_URI") + .addIndex("20180903.43", "IDX_SP_URI_HASH_URI") .unique(false) - .withColumns( "HASH_URI"); + .withColumns("HASH_URI"); spidxUri .addTask(new CalculateHashesTask(VersionEnum.V3_5_0, "20180903.44") .setColumnName("HASH_IDENTITY") @@ -885,7 +937,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { trmConcept .addIndex("20180903.51", "IDX_CONCEPT_UPDATED") .unique(false) - .withColumns( "CONCEPT_UPDATED"); + .withColumns("CONCEPT_UPDATED"); trmConcept .modifyColumn("20180903.52", "CODE") .nonNullable() diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java new file mode 100644 index 00000000000..4969c20d2a6 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageEntity.java @@ -0,0 +1,114 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; +import java.util.Date; +import java.util.List; + +@Entity() +@Table(name = "NPM_PACKAGE", uniqueConstraints = { + @UniqueConstraint(name = "IDX_PACK_ID", columnNames = "PACKAGE_ID") +}) +public class NpmPackageEntity { + + protected static final int PACKAGE_ID_LENGTH = 200; + + @SequenceGenerator(name = "SEQ_NPM_PACK", sequenceName = "SEQ_NPM_PACK") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_NPM_PACK") + @Id + @Column(name = "PID") + private Long myId; + @Column(name = "PACKAGE_ID", length = PACKAGE_ID_LENGTH, nullable = false) + private String myPackageId; + @Column(name = "CUR_VERSION_ID", length = NpmPackageVersionEntity.VERSION_ID_LENGTH, nullable = true) + private String myCurrentVersionId; + @Temporal(TemporalType.TIMESTAMP) + @Version + @Column(name = "UPDATED_TIME", nullable = false) + private Date myVersion; + @Column(name = "PACKAGE_DESC", length = NpmPackageVersionEntity.VERSION_ID_LENGTH, nullable = true) + private String myDescription; + @OneToMany(mappedBy = "myPackage") + private List myVersions; + + public String getDescription() { + return myDescription; + } + + public void setDescription(String theDescription) { + myDescription = theDescription; + } + + public String getPackageId() { + return myPackageId; + } + + public void setPackageId(String thePackageId) { + myPackageId = thePackageId; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) { + return true; + } + + if (theO == null || getClass() != theO.getClass()) { + return false; + } + + NpmPackageEntity that = (NpmPackageEntity) theO; + + return new EqualsBuilder() + .append(myPackageId, that.myPackageId) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(myPackageId) + .toHashCode(); + } + + public String getCurrentVersionId() { + return myCurrentVersionId; + } + + public void setCurrentVersionId(String theCurrentVersionId) { + myCurrentVersionId = theCurrentVersionId; + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java new file mode 100644 index 00000000000..2669c5a6d43 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionEntity.java @@ -0,0 +1,192 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.util.StringUtil; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Version; +import java.util.Date; +import java.util.List; + +@Entity() +@Table(name = "NPM_PACKAGE_VER", uniqueConstraints = { +}, indexes = { + @Index(name = "IDX_PACKVER", columnList = "PACKAGE_ID,VERSION_ID", unique = true) +}) +public class NpmPackageVersionEntity { + + public static final int VERSION_ID_LENGTH = 200; + public static final int FHIR_VERSION_LENGTH = 10; + + @SequenceGenerator(name = "SEQ_NPM_PACKVER", sequenceName = "SEQ_NPM_PACKVER") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_NPM_PACKVER") + @Id + @Column(name = "PID") + private Long myId; + @Column(name = "PACKAGE_ID", length = NpmPackageEntity.PACKAGE_ID_LENGTH, nullable = false) + private String myPackageId; + @Column(name = "VERSION_ID", length = NpmPackageVersionEntity.VERSION_ID_LENGTH, nullable = false) + private String myVersionId; + @ManyToOne + @JoinColumn(name = "PACKAGE_PID", nullable = false, foreignKey = @ForeignKey(name = "FK_NPM_PKV_PKG")) + private NpmPackageEntity myPackage; + @OneToOne + @JoinColumn(name = "BINARY_RES_ID", referencedColumnName = "RES_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_NPM_PKV_RESID")) + private ResourceTable myPackageBinary; + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "SAVED_TIME", nullable = false) + private Date mySavedTime; + @Column(name = "PKG_DESC", nullable = false, length = 200) + private String myDescription; + @Column(name = "DESC_UPPER", nullable = false, length = 200) + private String myDescriptionUpper; + @Column(name = "CURRENT_VERSION", nullable = false) + private boolean myCurrentVersion; + @Column(name = "FHIR_VERSION_ID", length = NpmPackageVersionEntity.FHIR_VERSION_LENGTH, nullable = false) + private String myFhirVersionId; + @Enumerated(EnumType.STRING) + @Column(name = "FHIR_VERSION", length = NpmPackageVersionEntity.FHIR_VERSION_LENGTH, nullable = false) + private FhirVersionEnum myFhirVersion; + @Column(name = "PACKAGE_SIZE_BYTES", nullable = false) + private long myPackageSizeBytes; + @Temporal(TemporalType.TIMESTAMP) + @Version + @Column(name = "UPDATED_TIME", nullable = false) + private Date myUpdatedTime; + @OneToMany(mappedBy = "myPackageVersion") + private List myResources; + + public Date getUpdatedTime() { + return myUpdatedTime; + } + + public long getPackageSizeBytes() { + return myPackageSizeBytes; + } + + public void setPackageSizeBytes(long thePackageSizeBytes) { + myPackageSizeBytes = thePackageSizeBytes; + } + + public boolean isCurrentVersion() { + return myCurrentVersion; + } + + public void setCurrentVersion(boolean theCurrentVersion) { + myCurrentVersion = theCurrentVersion; + } + + public String getPackageId() { + return myPackageId; + } + + public void setPackageId(String thePackageId) { + myPackageId = thePackageId; + } + + public String getVersionId() { + return myVersionId; + } + + public void setVersionId(String theVersionId) { + myVersionId = theVersionId; + } + + public String getFhirVersionId() { + return myFhirVersionId; + } + + public void setFhirVersionId(String theFhirVersionId) { + myFhirVersionId = theFhirVersionId; + } + + public FhirVersionEnum getFhirVersion() { + return myFhirVersion; + } + + public void setFhirVersion(FhirVersionEnum theFhirVersion) { + myFhirVersion = theFhirVersion; + } + + public NpmPackageEntity getPackage() { + return myPackage; + } + + public void setPackage(NpmPackageEntity thePackage) { + myPackage = thePackage; + } + + public ResourceTable getPackageBinary() { + return myPackageBinary; + } + + public void setPackageBinary(ResourceTable thePackageBinary) { + myPackageBinary = thePackageBinary; + } + + public void setSavedTime(Date theSavedTime) { + mySavedTime = theSavedTime; + } + + public String getDescription() { + return myDescription; + } + + public void setDescription(String theDescription) { + myDescription = theDescription; + myDescriptionUpper = StringUtil.normalizeStringForSearchIndexing(theDescription); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("myId", myId) + .append("myPackageId", myPackageId) + .append("myVersionId", myVersionId) + .append("myDescriptionUpper", myDescriptionUpper) + .append("myFhirVersionId", myFhirVersionId) + .toString(); + } + + public List getResources() { + return myResources; + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java new file mode 100644 index 00000000000..682b031a8f3 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/NpmPackageVersionResourceEntity.java @@ -0,0 +1,160 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirVersionEnum; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Version; +import java.util.Date; + +@Entity() +@Table(name = "NPM_PACKAGE_VER_RES", uniqueConstraints = { +}, indexes = { + @Index(name = "IDX_PACKVERRES_URL", columnList = "CANONICAL_URL") +}) +public class NpmPackageVersionResourceEntity { + + @Id + @SequenceGenerator(name = "SEQ_NPM_PACKVERRES", sequenceName = "SEQ_NPM_PACKVERRES") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_NPM_PACKVERRES") + @Column(name = "PID") + private Long myId; + @ManyToOne + @JoinColumn(name = "PACKVER_PID", referencedColumnName = "PID", foreignKey = @ForeignKey(name = "FK_NPM_PACKVERRES_PACKVER"), nullable = false) + private NpmPackageVersionEntity myPackageVersion; + @OneToOne + @JoinColumn(name = "BINARY_RES_ID", referencedColumnName = "RES_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_NPM_PKVR_RESID")) + private ResourceTable myResourceBinary; + @Column(name = "FILE_DIR", length = 200) + private String myDirectory; + @Column(name = "FILE_NAME", length = 200) + private String myFilename; + @Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false) + private String myResourceType; + @Column(name = "CANONICAL_URL", length = 200) + private String myCanonicalUrl; + @Column(name = "CANONICAL_VERSION", length = 200) + private String myCanonicalVersion; + @Enumerated(EnumType.STRING) + @Column(name = "FHIR_VERSION", length = NpmPackageVersionEntity.FHIR_VERSION_LENGTH, nullable = false) + private FhirVersionEnum myFhirVersion; + @Column(name = "FHIR_VERSION_ID", length = NpmPackageVersionEntity.FHIR_VERSION_LENGTH, nullable = false) + private String myFhirVersionId; + @Column(name = "RES_SIZE_BYTES", nullable = false) + private long myResSizeBytes; + @Temporal(TemporalType.TIMESTAMP) + @Version + @Column(name = "UPDATED_TIME", nullable = false) + private Date myVersion; + + public long getResSizeBytes() { + return myResSizeBytes; + } + + public void setResSizeBytes(long theResSizeBytes) { + myResSizeBytes = theResSizeBytes; + } + + public String getCanonicalVersion() { + return myCanonicalVersion; + } + + public void setCanonicalVersion(String theCanonicalVersion) { + myCanonicalVersion = theCanonicalVersion; + } + + public ResourceTable getResourceBinary() { + return myResourceBinary; + } + + public void setResourceBinary(ResourceTable theResourceBinary) { + myResourceBinary = theResourceBinary; + } + + public String getFhirVersionId() { + return myFhirVersionId; + } + + public void setFhirVersionId(String theFhirVersionId) { + myFhirVersionId = theFhirVersionId; + } + + public FhirVersionEnum getFhirVersion() { + return myFhirVersion; + } + + public void setFhirVersion(FhirVersionEnum theFhirVersion) { + myFhirVersion = theFhirVersion; + } + + public void setPackageVersion(NpmPackageVersionEntity thePackageVersion) { + myPackageVersion = thePackageVersion; + } + + public String getDirectory() { + return myDirectory; + } + + public void setDirectory(String theDirectory) { + myDirectory = theDirectory; + } + + public String getFilename() { + return myFilename; + } + + public void setFilename(String theFilename) { + myFilename = theFilename; + } + + public String getResourceType() { + return myResourceType; + } + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + public String getCanonicalUrl() { + return myCanonicalUrl; + } + + public void setCanonicalUrl(String theCanonicalUrl) { + myCanonicalUrl = theCanonicalUrl; + } + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java index 0e68f8e3903..17332450e7b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java @@ -24,7 +24,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.util.StringNormalizer; +import ca.uhn.fhir.util.StringUtil; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -266,7 +266,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return false; } StringParam string = (StringParam) theParam; - String normalizedString = StringNormalizer.normalizeStringForSearchIndexing(defaultString(string.getValue())); + String normalizedString = StringUtil.normalizeStringForSearchIndexing(defaultString(string.getValue())); return defaultString(getValueNormalized()).startsWith(normalizedString); } diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringNormalizerTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringNormalizerTest.java deleted file mode 100644 index 01a687fa89f..00000000000 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringNormalizerTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package ca.uhn.fhir.jpa.model.util; - -import ca.uhn.fhir.util.StringNormalizer; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class StringNormalizerTest { - @Test - public void testNormalizeString() { - assertEquals("TEST TEST", StringNormalizer.normalizeStringForSearchIndexing("TEST teSt")); - assertEquals("AEIØU", StringNormalizer.normalizeStringForSearchIndexing("åéîøü")); - assertEquals("杨浩", StringNormalizer.normalizeStringForSearchIndexing("杨浩")); - } -} diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringUtilTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringUtilTest.java new file mode 100644 index 00000000000..ee71b049e8c --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringUtilTest.java @@ -0,0 +1,45 @@ +package ca.uhn.fhir.jpa.model.util; + +import ca.uhn.fhir.util.StringUtil; +import com.google.common.base.Charsets; +import org.junit.Test; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; + +public class StringUtilTest { + @Test + public void testNormalizeString() { + assertEquals("TEST TEST", StringUtil.normalizeStringForSearchIndexing("TEST teSt")); + assertEquals("AEIØU", StringUtil.normalizeStringForSearchIndexing("åéîøü")); + assertEquals("杨浩", StringUtil.normalizeStringForSearchIndexing("杨浩")); + assertEquals(null, StringUtil.normalizeStringForSearchIndexing(null)); + } + + @Test + public void testToStringNoBom() { + String input = "help i'm a bug"; + String output = StringUtil.toUtf8String(input.getBytes(Charsets.UTF_8)); + assertEquals(input, output); + } + + @Test + public void testToStringWithBom() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(bos, StandardCharsets.UTF_8)); + out.write('\ufeff'); + out.write("help i'm a bug"); + out.close(); + + byte[] bytes = bos.toByteArray(); + String output = StringUtil.toUtf8String(bytes); + assertEquals("help i'm a bug", output); + } + + +} diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java index 67654101617..46929da4537 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java @@ -43,7 +43,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.model.primitive.BoundCodeDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.StringNormalizer; +import ca.uhn.fhir.util.StringUtil; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; @@ -1098,7 +1098,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } String searchParamName = theSearchParam.getName(); - String valueNormalized = StringNormalizer.normalizeStringForSearchIndexing(value); + String valueNormalized = StringUtil.normalizeStringForSearchIndexing(value); if (valueNormalized.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { valueNormalized = valueNormalized.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java index e910b0a4403..4a62691398b 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3Test.java @@ -19,7 +19,7 @@ import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; -import ca.uhn.fhir.util.StringNormalizer; +import ca.uhn.fhir.util.StringUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; import org.hamcrest.Matchers; @@ -74,7 +74,7 @@ public class SearchParamExtractorDstu3Test { String value = IntStream.range(1, 200).mapToObj(v -> "a").collect(Collectors.joining()) + "ئ"; assertEquals(value.length(), 200); assertEquals(Normalizer.normalize(value, Normalizer.Form.NFD).length(), 201); - assertEquals(StringNormalizer.normalizeStringForSearchIndexing(value).length(), 201); + assertEquals(StringUtil.normalizeStringForSearchIndexing(value).length(), 201); Questionnaire questionnaire = new Questionnaire(); questionnaire.setDescription(value); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index 872ac53a597..59c01e3da7d 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor; import org.apache.commons.dbcp2.BasicDataSource; @@ -133,15 +134,16 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { /** * Bean which validates incoming requests + * @param theInstanceValidator */ @Bean @Lazy - public RequestValidatingInterceptor requestValidatingInterceptor() { + public RequestValidatingInterceptor requestValidatingInterceptor(IValidatorModule theInstanceValidator) { RequestValidatingInterceptor requestValidator = new RequestValidatingInterceptor(); requestValidator.setFailOnSeverity(null); requestValidator.setAddResponseHeaderOnSeverity(null); requestValidator.setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum.INFORMATION); - requestValidator.addValidatorModule(instanceValidator()); + requestValidator.addValidatorModule(theInstanceValidator); requestValidator.setIgnoreValidatorExceptions(true); return requestValidator; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java index c4639c8e08e..045c1015e76 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.empi.rules.similarity; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.empi.util.NameUtil; -import ca.uhn.fhir.util.StringNormalizer; +import ca.uhn.fhir.util.StringUtil; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBase; @@ -56,10 +56,10 @@ public class NameSimilarity implements IEmpiFieldSimilarity { List rightGivenNames = NameUtil.extractGivenNames(theFhirContext, theRightBase); if (!exact) { - leftFamilyName = StringNormalizer.normalizeStringForSearchIndexing(leftFamilyName); - rightFamilyName = StringNormalizer.normalizeStringForSearchIndexing(rightFamilyName); - leftGivenNames = leftGivenNames.stream().map(StringNormalizer::normalizeStringForSearchIndexing).collect(Collectors.toList()); - rightGivenNames = rightGivenNames.stream().map(StringNormalizer::normalizeStringForSearchIndexing).collect(Collectors.toList()); + leftFamilyName = StringUtil.normalizeStringForSearchIndexing(leftFamilyName); + rightFamilyName = StringUtil.normalizeStringForSearchIndexing(rightFamilyName); + leftGivenNames = leftGivenNames.stream().map(StringUtil::normalizeStringForSearchIndexing).collect(Collectors.toList()); + rightGivenNames = rightGivenNames.stream().map(StringUtil::normalizeStringForSearchIndexing).collect(Collectors.toList()); } for (String leftGivenName : leftGivenNames) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index db3b8d23b76..f80f77bed8b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -100,6 +100,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.jar.Manifest; import java.util.stream.Collectors; +import static ca.uhn.fhir.util.StringUtil.toUtf8String; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -920,7 +921,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServerhelp im a bug"); + bundle.addEntry().setResource(patient); + + Bundle embeddedBundle = new Bundle(); + embeddedBundle.setType(Bundle.BundleType.COLLECTION); + bundle.addEntry().setResource(embeddedBundle); + + ResourceUtil.removeNarrative(FhirContext.forR4(), bundle); + + assertNull(((Patient)bundle.getEntry().get(0).getResource()).getText().getDiv().getValueAsString()); + } + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 6b79ca07119..829b9977ad7 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -1,21 +1,27 @@ package org.hl7.fhir.common.hapi.validation.support; +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Function; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; @SuppressWarnings("unchecked") public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport { @@ -30,17 +36,17 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple super(theWrap.getFhirContext(), theWrap); myValidateCodeCache = Caffeine .newBuilder() - .expireAfterWrite(60, TimeUnit.SECONDS) + .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(5000) .build(); myLookupCodeCache = Caffeine .newBuilder() - .expireAfterWrite(60, TimeUnit.SECONDS) + .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(5000) .build(); myCache = Caffeine .newBuilder() - .expireAfterWrite(60, TimeUnit.SECONDS) + .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(5000) .build(); } @@ -96,6 +102,19 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple } + @Override + public IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { + + BaseRuntimeChildDefinition urlChild = myCtx.getResourceDefinition(theValueSet).getChildByName("url"); + Optional valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream().map(t -> ((IPrimitiveType) t).getValueAsString()).filter(t->isNotBlank(t)).findFirst(); + if (valueSetUrl.isPresent()) { + String key = "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem, "(null)") + " " + defaultString(theCode, "(null)") + " " + defaultString(theDisplay, "(null)") + " " + valueSetUrl.get(); + return loadFromCache(myValidateCodeCache, key, t-> super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet)); + } + + return super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet); + } + @Override public void invalidateCaches() { myLookupCodeCache.invalidateAll(); diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ResourceMinimizerMojo.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ResourceMinimizerMojo.java index 1517ae9d658..10007924db1 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ResourceMinimizerMojo.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ResourceMinimizerMojo.java @@ -2,12 +2,10 @@ package ca.uhn.fhir.tinder; import java.io.*; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Collection; -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementDefinition; -import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.ResourceUtil; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -15,7 +13,6 @@ import org.apache.maven.plugin.*; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.FhirContext; @@ -72,7 +69,7 @@ public class ResourceMinimizerMojo extends AbstractMojo { String inputString; try { - inputString = IOUtils.toString(new FileInputStream(nextFile), "UTF-8"); + inputString = IOUtils.toString(new FileInputStream(nextFile), StandardCharsets.UTF_8); } catch (IOException e) { throw new MojoFailureException("Failed to read file: " + nextFile, e); } @@ -81,18 +78,18 @@ public class ResourceMinimizerMojo extends AbstractMojo { IBaseResource input = parser.parseResource(inputString); if (input instanceof IResource) { - ((IResource) input).getText().getDiv().setValueAsString((String) null); - ((IResource) input).getText().getStatus().setValueAsString((String) null); + ((IResource) input).getText().getDiv().setValueAsString(null); + ((IResource) input).getText().getStatus().setValueAsString(null); if (input instanceof Bundle) { for (Entry nextEntry : ((Bundle) input).getEntry()) { if (nextEntry.getResource() != null) { - nextEntry.getResource().getText().getDiv().setValueAsString((String) null); - nextEntry.getResource().getText().getStatus().setValueAsString((String) null); + nextEntry.getResource().getText().getDiv().setValueAsString(null); + nextEntry.getResource().getText().getStatus().setValueAsString(null); } } } } else { - minimizeResource((IBaseResource)input); + ResourceUtil.removeNarrative(myCtx, input); } String outputString = parser.setPrettyPrint(true).encodeResourceToString(input); @@ -117,7 +114,7 @@ public class ResourceMinimizerMojo extends AbstractMojo { myFileCount++; try { String f = nextFile.getAbsolutePath(); - Writer w = new OutputStreamWriter(new FileOutputStream(f, false), "UTF-8"); + Writer w = new OutputStreamWriter(new FileOutputStream(f, false), StandardCharsets.UTF_8); w = new BufferedWriter(w); w.append(outputString); w.close(); @@ -130,20 +127,6 @@ public class ResourceMinimizerMojo extends AbstractMojo { } } - private void minimizeResource(IBaseResource theInput) { - if (theInput instanceof IBaseBundle) { - for (IBaseResource next : BundleUtil.toListOfResources(myCtx, (IBaseBundle) theInput)) { - minimizeResource(next); - } - } - - BaseRuntimeElementCompositeDefinition element = (BaseRuntimeElementCompositeDefinition) myCtx.getElementDefinition(theInput.getClass()); - BaseRuntimeChildDefinition textElement = element.getChildByName("text"); - if (textElement != null) { - textElement.getMutator().setValue(theInput, null); - } - } - public long getByteCount() { return myByteCount; } diff --git a/pom.xml b/pom.xml index eb2fa5ac10e..a8934ea8b4c 100644 --- a/pom.xml +++ b/pom.xml @@ -665,7 +665,7 @@ - 5.0.1-SNAPSHOT + 5.0.7-SNAPSHOT 1.0.2 -Dfile.encoding=UTF-8 -Xmx2048m @@ -895,7 +895,7 @@ org.hl7.fhir.testcases fhir-test-cases - 1.1.14-SNAPSHOT + 1.1.14 org.jetbrains @@ -1009,6 +1009,11 @@ reflow-velocity-tools 2.0.0-beta2 + + io.swagger + swagger-annotations + 1.6.1 + mysql mysql-connector-java