diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 420b259a9cc..5aecb406cba 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,16 +25,6 @@ jobs: inputs: targetType: 'inline' script: mkdir -p $(MAVEN_CACHE_FOLDER); pwd; ls -al $(MAVEN_CACHE_FOLDER) - # - task: Maven@3 - #env: - # JAVA_HOME_11_X64: /usr/local/openjdk-11 - # inputs: - # goals: 'dependency:go-offline' - # # These are Maven CLI options (and show up in the build logs) - "-nsu"=Don't update snapshots. We can remove this when Maven OSS is more healthy - # options: '-P ALLMODULES,JACOCO,CI,ERRORPRONE -e -B -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)' - # # These are JVM options (and don't show up in the build logs) - # mavenOptions: '-Xmx1024m $(MAVEN_OPTS) -Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss,SSS -Duser.timezone=America/Toronto' - # jdkVersionOption: 1.11 - task: Maven@3 env: JAVA_HOME_11_X64: /usr/local/openjdk-11 diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/ApacheEncoder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/ApacheEncoder.java index 11bd84abf0d..2f2c11838df 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/ApacheEncoder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/ApacheEncoder.java @@ -22,9 +22,12 @@ package ca.uhn.fhir.context.phonetic; import org.apache.commons.codec.EncoderException; import org.apache.commons.codec.StringEncoder; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.StringJoiner; + public class ApacheEncoder implements IPhoneticEncoder { private static final Logger ourLog = LoggerFactory.getLogger(ApacheEncoder.class); @@ -44,10 +47,44 @@ public class ApacheEncoder implements IPhoneticEncoder { @Override public String encode(String theString) { try { + // If the string contains a space, encode alpha parts separately so, for example, numbers are preserved in address lines. + if (theString.contains(" ")) { + return encodeStringWithSpaces(theString); + } return myStringEncoder.encode(theString); } catch (EncoderException e) { ourLog.error("Failed to encode string " + theString, e); return theString; } } + + private String encodeStringWithSpaces(String theString) throws EncoderException { + StringJoiner joiner = new StringJoiner(" "); + + // This sub-stack holds the alpha parts + StringJoiner alphaJoiner = new StringJoiner(" "); + + for (String part : theString.split("[\\s\\W]+")) { + if (StringUtils.isAlpha(part)) { + alphaJoiner.add(part); + } else { + // Once we hit a non-alpha part, encode all the alpha parts together as a single string + // This is to allow encoders like METAPHONE to match Hans Peter to Hanspeter + alphaJoiner = encodeAlphaParts(joiner, alphaJoiner); + joiner.add(part); + } + } + encodeAlphaParts(joiner, alphaJoiner); + + return joiner.toString(); + } + + private StringJoiner encodeAlphaParts(StringJoiner theJoiner, StringJoiner theAlphaJoiner) throws EncoderException { + // Encode the alpha parts as a single string and then flush the alpha encoder + if (theAlphaJoiner.length() > 0) { + theJoiner.add(myStringEncoder.encode(theAlphaJoiner.toString())); + theAlphaJoiner = new StringJoiner(" "); + } + return theAlphaJoiner; + } } 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 f70fefca646..bc7365c15db 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 @@ -25,6 +25,9 @@ import org.apache.commons.lang3.builder.ToStringStyle; public class ConceptValidationOptions { + private boolean myValidateDisplay; + private boolean myInferSystem; + public boolean isInferSystem() { return myInferSystem; } @@ -34,12 +37,19 @@ public class ConceptValidationOptions { return this; } - private boolean myInferSystem; - @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .append("inferSystem", myInferSystem) .toString(); } + + public boolean isValidateDisplay() { + return myValidateDisplay; + } + + public ConceptValidationOptions setValidateDisplay(boolean theValidateDisplay) { + myValidateDisplay = theValidateDisplay; + return this; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java index 2d017578020..5db22381a69 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.context.support; * #L% */ +import org.thymeleaf.util.Validate; + import java.util.HashSet; import java.util.Set; @@ -29,6 +31,7 @@ public class ValidationSupportContext { private final Set myCurrentlyGeneratingSnapshots = new HashSet<>(); public ValidationSupportContext(IValidationSupport theRootValidationSupport) { + Validate.notNull(theRootValidationSupport, "theRootValidationSupport musty not be null"); myRootValidationSupport = theRootValidationSupport; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index 6626feee738..ec6bf9ee932 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -854,6 +854,21 @@ public enum Pointcut { */ SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED(void.class, "ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription"), + /** + * Subscription Hook: + * Invoked immediately after an active subscription is "registered". In HAPI FHIR, when + * a subscription + *

+ * Hooks may make changes to the canonicalized subscription and this will have an effect + * on processing across this server. Note however that timing issues may occur, since the + * subscription is already technically live by the time this hook is called. + *

+ * No parameters are currently supported. + *

+ * Hooks should return void. + *

+ */ + SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED(void.class), /** * Storage Hook: @@ -1562,6 +1577,42 @@ public enum Pointcut { "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" ), + /** + * Storage Hook: + * Invoked when a transaction has been rolled back as a result of a {@link ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException}, + * meaning that a database constraint has been violated. This pointcut allows an interceptor to specify a resolution strategy + * other than simply returning the error to the client. This interceptor will be fired after the database transaction rollback + * has been completed. + *

+ * Hooks may accept the following parameters: + *

+ * + *

+ * Hooks should return ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy. Hooks should not + * throw any exception. + *

+ */ + STORAGE_VERSION_CONFLICT( + "ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy", + "ca.uhn.fhir.rest.api.server.RequestDetails", + "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" + ), + /** * EMPI Hook: * Invoked whenever a persisted Patient/Practitioner resource (a resource that has just been stored in the diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQL.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQL.java index b3535a2ff43..5cb0541fcfc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQL.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQL.java @@ -20,6 +20,9 @@ package ca.uhn.fhir.rest.annotation; * #L% */ +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import org.hl7.fhir.instance.model.api.IBaseResource; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -32,4 +35,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(value= ElementType.METHOD) public @interface GraphQL { + RequestTypeEnum type() default RequestTypeEnum.GET; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQuery.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQueryBody.java similarity index 97% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQuery.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQueryBody.java index 71b427c4c51..a2494171168 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQuery.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQueryBody.java @@ -37,6 +37,6 @@ import java.lang.annotation.Target; */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) -public @interface GraphQLQuery { +public @interface GraphQLQueryBody { // ignore } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQueryUrl.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQueryUrl.java new file mode 100644 index 00000000000..12fffa35635 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GraphQLQueryUrl.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.rest.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 LBoicense. + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation should be placed on the parameter of a + * {@link GraphQL @GraphQL} annotated method. The given + * parameter will be populated with the specific graphQL + * query being requested. + * + *

+ * This parameter should be of type {@link String} + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface GraphQLQueryUrl { + // ignore +} 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 b337bea0eb0..15b6186a55f 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 @@ -66,6 +66,7 @@ public class Constants { public static final String CT_HTML = "text/html"; public static final String CT_HTML_WITH_UTF8 = "text/html" + CHARSET_UTF8_CTSUFFIX; public static final String CT_JSON = "application/json"; + public static final String CT_GRAPHQL = "application/graphql"; public static final String CT_JSON_PATCH = "application/json-patch+json"; public static final String CT_OCTET_STREAM = "application/octet-stream"; public static final String CT_TEXT = "text/plain"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java index caf0529f272..cae787c4868 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.java @@ -37,6 +37,9 @@ public class ResourceVersionConflictException extends BaseServerResponseExceptio public static final int STATUS_CODE = Constants.STATUS_HTTP_409_CONFLICT; private static final long serialVersionUID = 1L; + /** + * Constructor + */ public ResourceVersionConflictException(String error) { super(STATUS_CODE, error); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index fa7306678c1..2be7cbc7206 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -13,6 +13,8 @@ import ca.uhn.fhir.context.RuntimeExtensionDtDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.model.api.ExtensionDt; +import ca.uhn.fhir.model.api.IElement; +import ca.uhn.fhir.model.api.IIdentifiableElement; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions; import ca.uhn.fhir.model.base.composite.BaseContainedDt; @@ -21,6 +23,7 @@ import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.DataFormatException; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseElement; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions; @@ -128,6 +131,28 @@ public class FhirTerser { Validate.notNull(theSource, "theSource must not be null"); Validate.notNull(theTarget, "theTarget must not be null"); + // DSTU3+ + if (theSource instanceof IBaseElement) { + IBaseElement source = (IBaseElement) theSource; + IBaseElement target = (IBaseElement) theTarget; + target.setId(source.getId()); + } + + // DSTU2 only + if (theSource instanceof IIdentifiableElement) { + IIdentifiableElement source = (IIdentifiableElement) theSource; + IIdentifiableElement target = (IIdentifiableElement) theTarget; + target.setElementSpecificId(source.getElementSpecificId()); + } + + // DSTU2 only + if (theSource instanceof IResource) { + IResource source = (IResource) theSource; + IResource target = (IResource) theTarget; + target.setId(source.getId()); + target.getResourceMetadata().putAll(source.getResourceMetadata()); + } + if (theSource instanceof IPrimitiveType) { if (theTarget instanceof IPrimitiveType) { ((IPrimitiveType) theTarget).setValueAsString(((IPrimitiveType) theSource).getValueAsString()); @@ -159,7 +184,13 @@ public class FhirTerser { } BaseRuntimeElementDefinition element = myContext.getElementDefinition(nextValue.getClass()); - IBase target = element.newInstance(); + Object instanceConstructorArg = targetChild.getInstanceConstructorArguments(); + IBase target; + if (instanceConstructorArg != null) { + target = element.newInstance(instanceConstructorArg); + } else { + target = element.newInstance(); + } targetChild.getMutator().addValue(theTarget, target); cloneInto(nextValue, target, theIgnoreMissingFields); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java new file mode 100644 index 00000000000..bca150978cb --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.context.phonetic; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; + +class PhoneticEncoderTest { + private static final Logger ourLog = LoggerFactory.getLogger(PhoneticEncoderTest.class); + + private static final String NUMBER = "123"; + private static final String STREET = "Nohili St, Suite"; + private static final String SUITE = "456"; + private static final String ADDRESS_LINE = NUMBER + " " + STREET + " " + SUITE; + + @ParameterizedTest + @EnumSource(PhoneticEncoderEnum.class) + public void testEncodeAddress(PhoneticEncoderEnum thePhoneticEncoderEnum) { + String encoded = thePhoneticEncoderEnum.getPhoneticEncoder().encode(ADDRESS_LINE); + ourLog.info("{}: {}", thePhoneticEncoderEnum.name(), encoded); + assertThat(encoded, startsWith(NUMBER + " ")); + assertThat(encoded, endsWith(" " + SUITE)); + } +} 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 index 123335e06b2..bd7db34d1a6 100644 --- 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 @@ -134,14 +134,16 @@ public class CreatePackageCommand extends BaseCommand { } String[] dependencies = theCommandLine.getOptionValues(DEPENDENCY_OPT); - for (String nextDependencyString : dependencies) { - int colonIdx = nextDependencyString.indexOf(":"); - if (colonIdx == -1) { - throw new ParseException("Invalid dependency spec: " + nextDependencyString); + if (dependencies != null) { + 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); } - String depName = nextDependencyString.substring(0, colonIdx); - String depVersion = nextDependencyString.substring(colonIdx+1); - manifestGenerator.dependency(depName, depVersion); } myWorkDirectory = Files.createTempDir(); 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 index fe00f139118..8aa4deb5b4c 100644 --- 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 @@ -113,6 +113,54 @@ public class CreatePackageCommandTest extends BaseTest { } + @Test + public void testCreatePackage_NoDependencies() 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() + }); + + 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" + + "}"; + 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/java/ca/uhn/hapi/fhir/docs/ServerOperations.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServerOperations.java index d875b866f5e..f571db9f020 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServerOperations.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServerOperations.java @@ -53,9 +53,9 @@ public class ServerOperations { ourLog.info("Received call with content type {} and {} bytes", contentType, bytes.length); - theServletResponse.setContentType(contentType); - theServletResponse.getOutputStream().write(bytes); - theServletResponse.getOutputStream().close(); + theServletResponse.setContentType("text/plain"); + theServletResponse.getWriter().write("hello"); + theServletResponse.getWriter().close(); } //END SNIPPET: manualInputAndOutput diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1896-add-support-for-graphql-body.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1896-add-support-for-graphql-body.yaml new file mode 100644 index 00000000000..8d7266ae010 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1896-add-support-for-graphql-body.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1896 +title: "Support has been added for GraphQL querying using an HTTP POST (with the query in the body). Thanks to + Ibrohim Kholilul Islam for the pull request implementing this new feature!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1966-allow-graphql-or-list.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1966-allow-graphql-or-list.yaml new file mode 100644 index 00000000000..c579d968712 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1966-allow-graphql-or-list.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 1966 +title: "The GraphQL module can now accept arrays of arguments as input to searches, and will treat them as + OR'ed parameters. Thanks to Ibrohim Kholilul Isla for the pull request!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1971-avoid-concurrent-write-errors.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1971-avoid-concurrent-write-errors.yaml new file mode 100644 index 00000000000..616a2137289 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1971-avoid-concurrent-write-errors.yaml @@ -0,0 +1,6 @@ +--- +type: add +issue: 1971 +title: "A new interceptor called `UserRequestRetryVersionConflictsInterceptor` has been added to the JPA server. This interceptor + allows clients to instruct the server to attempt to avoid returning an HTTP 409 (Version Conflict) if two concurrent client + requests try to update the same resource at the same time." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1971-create-package-no-dependencies.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1971-create-package-no-dependencies.yaml new file mode 100644 index 00000000000..19285ae6da0 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1971-create-package-no-dependencies.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 1971 +title: The create-package CLI command failed with a NPE if no package dependencies were specified. This has been corrected. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1982-fix-validation-for-enumerated-valuesets.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1982-fix-validation-for-enumerated-valuesets.yaml new file mode 100644 index 00000000000..97af6a0ba50 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1982-fix-validation-for-enumerated-valuesets.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 1982 +title: The validator will now accept codes that are defined in a ValueSet where the valueset contains an enumeration of + codes, and the CodeSystem URL refers to an unknown CodeSystem. This allows successful validation of ValueSets in several + IGs that rely on the existence of grammar based systems. + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1983-allow-conceptmap-with-no-group-source_or_target.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1983-allow-conceptmap-with-no-group-source_or_target.yaml new file mode 100644 index 00000000000..bf2bd0b832c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1983-allow-conceptmap-with-no-group-source_or_target.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 1983 +title: "ConceptMap resources were blocked from uploading into the JPA server if the ConceptMap had a source + and/or target URL defined at the ConceptMap level but not at the group level. This prevented some US Core + resources from being successfully uploaded. This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index bb6fccad007..09b2bd28452 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -178,3 +178,10 @@ The ResponseSizeCapturingInterceptor can be used to capture the number of charac * [ResponseSizeCapturingInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/ResponseSizeCapturingInterceptor.html) * [ResponseSizeCapturingInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseSizeCapturingInterceptor.java) +# JPA Server: Allow Cascading Deletes + +The CascadingDeleteInterceptor allows clients to request deletes be cascaded to other resources that contain incoming references. See [Cascading Deletes](/docs/server_jpa/configuration.html#cascading-deletes) for more information. + +# JPA Server: Retry on Version Conflicts + +The UserRequestRetryVersionConflictsInterceptor allows clients to request that the server avoid version conflicts (HTTP 409) when two concurrent client requests attempt to modify the same resource. See [Version Conflicts](/docs/server_jpa/configuration.html#retry-on-version-conflict) for more information. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/configuration.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/configuration.md index 0b33e7c4f52..d8e759edd5f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/configuration.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/configuration.md @@ -103,10 +103,26 @@ Cache-Control: no-store, max-results=20 * [This page](https://www.openhealthhub.org/t/hapi-terminology-server-uk-snomed-ct-import/592) has information on loading national editions (UK specifically) of SNOMED CT files into the database. + + + # Cascading Deletes -An interceptor called `CascadingDeleteInterceptor` may be registered against the Server. When this interceptor is enabled, cascading deletes may be performed using either of the following: +An interceptor called `CascadingDeleteInterceptor` may be registered against the server. When this interceptor is enabled, cascading deletes may be performed using either of the following: * The request may include the following parameter: `_cascade=delete` * The request may include the following header: `X-Cascade: delete` + + +# Version Conflicts + +If a server is serving multiple concurrent requests against the same resource, a [ResourceVersionConflictException](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/server/exceptions/ResourceVersionConflictException.html) may be thrown (resulting in an **HTTP 409 Version Conflict** being returned to the client). For example, if two client requests attempt to update the same resource at the exact same time, this exception will be thrown for one of the requests. This exception is not a bug in the server itself, but instead is a defense against client updates accidentally being lost because of concurrency issues. When this occurs, it is important to consider what the root cause might be, since concurrent writes against the same resource are often indicative of a deeper application design issue. + +An interceptor called `UserRequestRetryVersionConflictsInterceptor` may be registered against the server. When this interceptor is enabled, requests may include an optional header requesting for the server to try to avoid returning an error due to concurrent writes. The server will then try to avoid version conflict errors by automatically retrying requests that would have otherwise failed due to a version conflict. + +With this interceptor in place, the following header can be added to individual HTTP requests to instruct the server to avoid version conflict errors: + +```http +X-Retry-On-Version-Conflict: retry; max-retries=100 +``` diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoValueSet.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoValueSet.java index acd038d9ef0..a044e2e262b 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoValueSet.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirResourceDaoValueSet.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.api.dao; * #L% */ +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -41,31 +42,6 @@ public interface IFhirResourceDaoValueSet exten void purgeCaches(); - ValidateCodeResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, IPrimitiveType theSystem, IPrimitiveType theDisplay, CD theCoding, CC theCodeableConcept, RequestDetails theRequestDetails); - - class ValidateCodeResult { - private String myDisplay; - private String myMessage; - private boolean myResult; - - public ValidateCodeResult(boolean theResult, String theMessage, String theDisplay) { - super(); - myResult = theResult; - myMessage = theMessage; - myDisplay = theDisplay; - } - - public String getDisplay() { - return myDisplay; - } - - public String getMessage() { - return myMessage; - } - - public boolean isResult() { - return myResult; - } - } + IValidationSupport.CodeValidationResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, IPrimitiveType theSystem, IPrimitiveType theDisplay, CD theCoding, CC theCodeableConcept, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/ResourceVersionConflictResolutionStrategy.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/ResourceVersionConflictResolutionStrategy.java new file mode 100644 index 00000000000..053d6d20187 --- /dev/null +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/ResourceVersionConflictResolutionStrategy.java @@ -0,0 +1,49 @@ +package ca.uhn.fhir.jpa.api.model; + +/*- + * #%L + * HAPI FHIR JPA 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 org.apache.commons.lang3.Validate; + +/** + * @since 5.1.0 + */ +public class ResourceVersionConflictResolutionStrategy { + + private int myMaxRetries; + private boolean myRetry; + + public int getMaxRetries() { + return myMaxRetries; + } + + public void setMaxRetries(int theMaxRetries) { + Validate.isTrue(theMaxRetries >= 0, "theRetryUpToMillis must not be negative"); + myMaxRetries = theMaxRetries; + } + + public boolean isRetry() { + return myRetry; + } + + public void setRetry(boolean theRetry) { + myRetry = theRetry; + } +} diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 895d78ff727..5dd85caa300 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -163,8 +163,6 @@ - - net.ttddyy datasource-proxy @@ -734,7 +732,8 @@ dstu2 ca.uhn.fhir.jpa.config ca.uhn.fhir.jpa.rp.dstu2 - hapi-fhir-server-resourceproviders-dstu2.xml + hapi-fhir-server-resourceproviders-dstu2.xml + @@ -750,7 +749,8 @@ dstu3 ca.uhn.fhir.jpa.config ca.uhn.fhir.jpa.rp.dstu3 - hapi-fhir-server-resourceproviders-dstu3.xml + hapi-fhir-server-resourceproviders-dstu3.xml + @@ -765,7 +765,8 @@ r4 ca.uhn.fhir.jpa.config ca.uhn.fhir.jpa.rp.r4 - hapi-fhir-server-resourceproviders-r4.xml + hapi-fhir-server-resourceproviders-r4.xml + @@ -780,7 +781,8 @@ r5 ca.uhn.fhir.jpa.config ca.uhn.fhir.jpa.rp.r5 - hapi-fhir-server-resourceproviders-r5.xml + hapi-fhir-server-resourceproviders-r5.xml + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java index b65110c8fd2..86e0203c411 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java @@ -28,6 +28,8 @@ import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.JobParametersValidator; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; import java.util.Arrays; import java.util.Optional; @@ -38,6 +40,8 @@ import java.util.Optional; public class BulkExportJobParameterValidator implements JobParametersValidator { @Autowired private IBulkExportJobDao myBulkExportJobDao; + @Autowired + private PlatformTransactionManager myTransactionManager; @Override public void validate(JobParameters theJobParameters) throws JobParametersInvalidException { @@ -45,41 +49,45 @@ public class BulkExportJobParameterValidator implements JobParametersValidator { throw new JobParametersInvalidException("This job needs Parameters: [readChunkSize], [jobUUID], [filters], [outputFormat], [resourceTypes]"); } - StringBuilder errorBuilder = new StringBuilder(); - Long readChunkSize = theJobParameters.getLong("readChunkSize"); - if (readChunkSize == null || readChunkSize < 1) { - errorBuilder.append("There must be a valid number for readChunkSize, which is at least 1. "); - } - String jobUUID = theJobParameters.getString("jobUUID"); - Optional oJob = myBulkExportJobDao.findByJobId(jobUUID); - if (!StringUtils.isBlank(jobUUID) && !oJob.isPresent()) { - errorBuilder.append("There is no persisted job that exists with UUID: " + jobUUID + ". "); - } - - - boolean hasExistingJob = oJob.isPresent(); - //Check for to-be-created parameters. - if (!hasExistingJob) { - String resourceTypes = theJobParameters.getString("resourceTypes"); - if (StringUtils.isBlank(resourceTypes)) { - errorBuilder.append("You must include [resourceTypes] as a Job Parameter"); - } else { - String[] resourceArray = resourceTypes.split(","); - Arrays.stream(resourceArray).filter(resourceType -> resourceType.equalsIgnoreCase("Binary")) - .findFirst() - .ifPresent(resourceType -> { - errorBuilder.append("Bulk export of Binary resources is forbidden"); - }); + TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); + String errorMessage = txTemplate.execute(tx -> { + StringBuilder errorBuilder = new StringBuilder(); + Long readChunkSize = theJobParameters.getLong("readChunkSize"); + if (readChunkSize == null || readChunkSize < 1) { + errorBuilder.append("There must be a valid number for readChunkSize, which is at least 1. "); } - - String outputFormat = theJobParameters.getString("outputFormat"); - if (!StringUtils.isBlank(outputFormat) && !Constants.CT_FHIR_NDJSON.equals(outputFormat)) { - errorBuilder.append("The only allowed format for Bulk Export is currently " + Constants.CT_FHIR_NDJSON); + String jobUUID = theJobParameters.getString("jobUUID"); + Optional oJob = myBulkExportJobDao.findByJobId(jobUUID); + if (!StringUtils.isBlank(jobUUID) && !oJob.isPresent()) { + errorBuilder.append("There is no persisted job that exists with UUID: " + jobUUID + ". "); } - } - String errorMessage = errorBuilder.toString(); + boolean hasExistingJob = oJob.isPresent(); + //Check for to-be-created parameters. + if (!hasExistingJob) { + String resourceTypes = theJobParameters.getString("resourceTypes"); + if (StringUtils.isBlank(resourceTypes)) { + errorBuilder.append("You must include [resourceTypes] as a Job Parameter"); + } else { + String[] resourceArray = resourceTypes.split(","); + Arrays.stream(resourceArray).filter(resourceType -> resourceType.equalsIgnoreCase("Binary")) + .findFirst() + .ifPresent(resourceType -> { + errorBuilder.append("Bulk export of Binary resources is forbidden"); + }); + } + + String outputFormat = theJobParameters.getString("outputFormat"); + if (!StringUtils.isBlank(outputFormat) && !Constants.CT_FHIR_NDJSON.equals(outputFormat)) { + errorBuilder.append("The only allowed format for Bulk Export is currently " + Constants.CT_FHIR_NDJSON); + } + + + } + return errorBuilder.toString(); + }); + if (!StringUtils.isEmpty(errorMessage)) { throw new JobParametersInvalidException(errorMessage); } 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 2647022b387..36a02f2c08e 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 @@ -21,6 +21,7 @@ import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; @@ -304,6 +305,11 @@ public abstract class BaseConfig { return new PersistenceExceptionTranslationPostProcessor(); } + @Bean + public HapiTransactionService hapiTransactionService() { + return new HapiTransactionService(); + } + @Bean public IInterceptorService jpaInterceptorService() { return new InterceptorService(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java index 1a8fb937b5f..b3e8e85e435 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java @@ -26,7 +26,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import org.hibernate.HibernateException; -import org.hibernate.StaleStateException; +import org.hibernate.PessimisticLockException; import org.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +35,7 @@ import org.springframework.orm.jpa.vendor.HibernateJpaDialect; import javax.persistence.PersistenceException; +import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect { @@ -76,13 +77,14 @@ public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect { * will return it as lowercase even though the definition is in caps. */ if (isNotBlank(constraintName)) { - if (constraintName.toUpperCase().contains(ResourceHistoryTable.IDX_RESVER_ID_VER)) { + constraintName = constraintName.toUpperCase(); + if (constraintName.contains(ResourceHistoryTable.IDX_RESVER_ID_VER)) { throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure")); } - if (constraintName.toUpperCase().contains(ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING)) { + if (constraintName.contains(ResourceIndexedCompositeStringUnique.IDX_IDXCMPSTRUNIQ_STRING)) { throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceIndexedCompositeStringUniqueConstraintFailure")); } - if (constraintName.toUpperCase().contains(ForcedId.IDX_FORCEDID_TYPE_FID)) { + if (constraintName.contains(ForcedId.IDX_FORCEDID_TYPE_FID)) { throw new ResourceVersionConflictException(messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "forcedIdConstraintFailure")); } } @@ -102,10 +104,18 @@ public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect { * class in a method called "checkBatched" currently. This can all be tested using the * StressTestR4Test method testMultiThreadedUpdateSameResourceInTransaction() */ - if (theException instanceof StaleStateException) { + if (theException instanceof org.hibernate.StaleStateException) { String msg = messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"); throw new ResourceVersionConflictException(msg); } + if (theException instanceof org.hibernate.PessimisticLockException) { + PessimisticLockException ex = (PessimisticLockException) theException; + String sql = defaultString(ex.getSQL()).toUpperCase(); + if (sql.contains(ResourceHistoryTable.HFJ_RES_VER)) { + String msg = messageToPrepend + myLocalizer.getMessage(HapiFhirHibernateJpaDialect.class, "resourceVersionConstraintFailure"); + throw new ResourceVersionConflictException(msg); + } + } return super.convertHibernateAccessException(theException); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 9abd7a8bc25..a6153a4c9b6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -79,6 +79,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; @@ -113,8 +114,10 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Repository; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; 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 144c498bbde..ecd7f1f518b 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 @@ -33,10 +33,9 @@ import ca.uhn.fhir.jpa.api.model.DeleteConflictList; import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.jpa.patch.FhirPatch; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.jpa.model.entity.BaseTag; import ca.uhn.fhir.jpa.model.entity.ForcedId; @@ -46,15 +45,15 @@ import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; +import ca.uhn.fhir.jpa.patch.FhirPatch; +import ca.uhn.fhir.jpa.patch.JsonPatchUtils; +import ca.uhn.fhir.jpa.patch.XmlPatchUtils; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; -import ca.uhn.fhir.jpa.patch.JsonPatchUtils; -import ca.uhn.fhir.jpa.patch.XmlPatchUtils; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.CacheControlDirective; @@ -70,6 +69,8 @@ import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; @@ -108,6 +109,7 @@ import org.springframework.transaction.support.TransactionSynchronizationAdapter import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; @@ -124,7 +126,6 @@ import java.util.UUID; import static org.apache.commons.lang3.StringUtils.isNotBlank; -@Transactional(propagation = Propagation.REQUIRED) public abstract class BaseHapiFhirResourceDao extends BaseHapiFhirDao implements IFhirResourceDao { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class); @@ -151,8 +152,11 @@ public abstract class BaseHapiFhirResourceDao extends B private IRequestPartitionHelperSvc myRequestPartitionHelperService; @Autowired private PartitionSettings myPartitionSettings; + @Autowired + private HapiTransactionService myTransactionService; @Override + @Transactional public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel, RequestDetails theRequest) { StopWatch w = new StopWatch(); BaseHasResource entity = readEntity(theId, theRequest); @@ -197,8 +201,20 @@ public abstract class BaseHapiFhirResourceDao extends B return create(theResource, theIfNoneExist, null); } + @Override + public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) { + return create(theResource, theIfNoneExist, true, new TransactionDetails(), theRequestDetails); + } + @Override public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) { + return myTransactionService.execute(theRequestDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails)); + } + + /** + * Called for FHIR create (POST) operations + */ + private DaoMethodOutcome doCreateForPost(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) { if (theResource == null) { String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody"); throw new InvalidRequestException(msg); @@ -220,241 +236,14 @@ public abstract class BaseHapiFhirResourceDao extends B } RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName()); - return doCreate(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails, requestPartitionId); - } - - @Override - public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) { - return create(theResource, theIfNoneExist, true, new TransactionDetails(), theRequestDetails); - } - - private IInstanceValidatorModule getInstanceValidator() { - return myInstanceValidator; - } - - @Override - public DaoMethodOutcome delete(IIdType theId) { - return delete(theId, null); - } - - @Override - public DaoMethodOutcome delete(IIdType theId, DeleteConflictList theDeleteConflicts, RequestDetails theRequest, TransactionDetails theTransactionDetails) { - validateIdPresentForDelete(theId); - validateDeleteEnabled(); - - final ResourceTable entity = readEntityLatestVersion(theId, theRequest); - if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) { - throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version"); - } - - // Don't delete again if it's already deleted - if (entity.getDeleted() != null) { - DaoMethodOutcome outcome = new DaoMethodOutcome(); - outcome.setEntity(entity); - - IIdType id = getContext().getVersion().newIdType(); - id.setValue(entity.getIdDt().getValue()); - outcome.setId(id); - - IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); - String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, 0); - String severity = "information"; - String code = "informational"; - OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); - outcome.setOperationOutcome(oo); - - return outcome; - } - - StopWatch w = new StopWatch(); - - T resourceToDelete = toResource(myResourceType, entity, null, false); - theDeleteConflicts.setResourceIdMarkedForDeletion(theId); - - // Notify IServerOperationInterceptors about pre-action call - HookParams hook = new HookParams() - .add(IBaseResource.class, resourceToDelete) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(TransactionDetails.class, theTransactionDetails); - doCallHooks(theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook); - - myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequest, theTransactionDetails); - - preDelete(resourceToDelete, entity); - - // Notify interceptors - if (theRequest != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId); - notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); - } - - ResourceTable savedEntity = updateEntityForDelete(theRequest, theTransactionDetails, entity); - resourceToDelete.setId(entity.getIdDt()); - - // Notify JPA interceptors - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { - @Override - public void beforeCommit(boolean readOnly) { - HookParams hookParams = new HookParams() - .add(IBaseResource.class, resourceToDelete) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(TransactionDetails.class, theTransactionDetails); - doCallHooks(theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); - } - }); - - DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, resourceToDelete).setCreated(true); - - IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); - String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis()); - String severity = "information"; - String code = "informational"; - OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); - outcome.setOperationOutcome(oo); - - return outcome; - } - - @Override - public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) { - validateIdPresentForDelete(theId); - validateDeleteEnabled(); - - DeleteConflictList deleteConflicts = new DeleteConflictList(); - if (isNotBlank(theId.getValue())) { - deleteConflicts.setResourceIdMarkedForDeletion(theId); - } - - StopWatch w = new StopWatch(); - - DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, new TransactionDetails()); - - DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); - - ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); - return retVal; + return doCreateForPostOrPut(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails, requestPartitionId); } /** - * This method gets called by {@link #deleteByUrl(String, DeleteConflictList, RequestDetails)} as well as by - * transaction processors + * Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails)} + * as well as for FHIR update (PUT) where we're doing a create-with-client-assigned-ID (via {@link #doUpdate(IBaseResource, String, boolean, boolean, RequestDetails, TransactionDetails)}. */ - @Override - public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) { - validateDeleteEnabled(); - - StopWatch w = new StopWatch(); - - Set resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType, theRequest); - if (resourceIds.size() > 1) { - if (myDaoConfig.isAllowMultipleDelete() == false) { - throw new PreconditionFailedException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size())); - } - } - - TransactionDetails transactionDetails = new TransactionDetails(); - List deletedResources = new ArrayList<>(); - for (ResourcePersistentId pid : resourceIds) { - ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId()); - deletedResources.add(entity); - - T resourceToDelete = toResource(myResourceType, entity, null, false); - - // Notify IServerOperationInterceptors about pre-action call - HookParams hooks = new HookParams() - .add(IBaseResource.class, resourceToDelete) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(TransactionDetails.class, transactionDetails); - doCallHooks(theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks); - - myDeleteConflictService.validateOkToDelete(deleteConflicts, entity, false, theRequest, transactionDetails); - - // Notify interceptors - IdDt idToDelete = entity.getIdDt(); - if (theRequest != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete); - notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); - } - - // Perform delete - - updateEntityForDelete(theRequest, transactionDetails, entity); - resourceToDelete.setId(entity.getIdDt()); - - // Notify JPA interceptors - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { - @Override - public void beforeCommit(boolean readOnly) { - HookParams hookParams = new HookParams() - .add(IBaseResource.class, resourceToDelete) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(TransactionDetails.class, transactionDetails); - doCallHooks(theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); - } - }); - } - - IBaseOperationOutcome oo; - if (deletedResources.isEmpty()) { - oo = OperationOutcomeUtil.newInstance(getContext()); - String message = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl); - String severity = "warning"; - String code = "not-found"; - OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); - } else { - oo = OperationOutcomeUtil.newInstance(getContext()); - String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", deletedResources.size(), w.getMillis()); - String severity = "information"; - String code = "informational"; - OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); - } - - ourLog.debug("Processed delete on {} (matched {} resource(s)) in {}ms", theUrl, deletedResources.size(), w.getMillis()); - - DeleteMethodOutcome retVal = new DeleteMethodOutcome(); - retVal.setDeletedEntities(deletedResources); - retVal.setOperationOutcome(oo); - return retVal; - } - - @Override - public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) { - validateDeleteEnabled(); - - DeleteConflictList deleteConflicts = new DeleteConflictList(); - - DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequestDetails); - - DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); - - return outcome; - } - - private void validateDeleteEnabled() { - if (!myDaoConfig.isDeleteEnabled()) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "deleteBlockedBecauseDisabled"); - throw new PreconditionFailedException(msg); - } - } - - private void validateIdPresentForDelete(IIdType theId) { - if (theId == null || !theId.hasIdPart()) { - throw new InvalidRequestException("Can not perform delete, no ID provided"); - } - } - - @PostConstruct - public void detectSearchDaoDisabled() { - if (mySearchDao != null && mySearchDao.isDisabled()) { - mySearchDao = null; - } - } - - private DaoMethodOutcome doCreate(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + private DaoMethodOutcome doCreateForPostOrPut(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { StopWatch w = new StopWatch(); preProcessResourceForStorage(theResource); @@ -579,6 +368,239 @@ public abstract class BaseHapiFhirResourceDao extends B return outcome; } + private IInstanceValidatorModule getInstanceValidator() { + return myInstanceValidator; + } + + @Override + public DaoMethodOutcome delete(IIdType theId) { + return delete(theId, null); + } + + @Override + public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) { + validateIdPresentForDelete(theId); + validateDeleteEnabled(); + + return myTransactionService.execute(theRequestDetails, tx -> { + DeleteConflictList deleteConflicts = new DeleteConflictList(); + if (isNotBlank(theId.getValue())) { + deleteConflicts.setResourceIdMarkedForDeletion(theId); + } + + StopWatch w = new StopWatch(); + + DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, new TransactionDetails()); + + DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); + + ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); + return retVal; + }); + } + + @Override + public DaoMethodOutcome delete(IIdType theId, DeleteConflictList theDeleteConflicts, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { + validateIdPresentForDelete(theId); + validateDeleteEnabled(); + + final ResourceTable entity = readEntityLatestVersion(theId, theRequestDetails); + if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) { + throw new ResourceVersionConflictException("Trying to delete " + theId + " but this is not the current version"); + } + + // Don't delete again if it's already deleted + if (entity.getDeleted() != null) { + DaoMethodOutcome outcome = new DaoMethodOutcome(); + outcome.setEntity(entity); + + IIdType id = getContext().getVersion().newIdType(); + id.setValue(entity.getIdDt().getValue()); + outcome.setId(id); + + IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); + String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, 0); + String severity = "information"; + String code = "informational"; + OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); + outcome.setOperationOutcome(oo); + + return outcome; + } + + StopWatch w = new StopWatch(); + + T resourceToDelete = toResource(myResourceType, entity, null, false); + theDeleteConflicts.setResourceIdMarkedForDeletion(theId); + + // Notify IServerOperationInterceptors about pre-action call + HookParams hook = new HookParams() + .add(IBaseResource.class, resourceToDelete) + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(TransactionDetails.class, theTransactionDetails); + doCallHooks(theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook); + + myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails); + + preDelete(resourceToDelete, entity); + + // Notify interceptors + if (theRequestDetails != null) { + ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theId.getResourceType(), theId); + notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); + } + + ResourceTable savedEntity = updateEntityForDelete(theRequestDetails, theTransactionDetails, entity); + resourceToDelete.setId(entity.getIdDt()); + + // Notify JPA interceptors + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void beforeCommit(boolean readOnly) { + HookParams hookParams = new HookParams() + .add(IBaseResource.class, resourceToDelete) + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(TransactionDetails.class, theTransactionDetails); + doCallHooks(theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); + } + }); + + DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, resourceToDelete).setCreated(true); + + IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); + String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", 1, w.getMillis()); + String severity = "information"; + String code = "informational"; + OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); + outcome.setOperationOutcome(oo); + + return outcome; + } + + @Override + public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequestDetails) { + validateDeleteEnabled(); + + return myTransactionService.execute(theRequestDetails, tx -> { + DeleteConflictList deleteConflicts = new DeleteConflictList(); + DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequestDetails); + DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); + return outcome; + }); + } + + /** + * This method gets called by {@link #deleteByUrl(String, RequestDetails)} as well as by + * transaction processors + */ + @Override + public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequestDetails) { + validateDeleteEnabled(); + + return myTransactionService.execute(theRequestDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theRequestDetails)); + } + + @Nonnull + private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) { + StopWatch w = new StopWatch(); + + Set resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType, theRequest); + if (resourceIds.size() > 1) { + if (myDaoConfig.isAllowMultipleDelete() == false) { + throw new PreconditionFailedException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size())); + } + } + + TransactionDetails transactionDetails = new TransactionDetails(); + List deletedResources = new ArrayList<>(); + for (ResourcePersistentId pid : resourceIds) { + ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId()); + deletedResources.add(entity); + + T resourceToDelete = toResource(myResourceType, entity, null, false); + + // Notify IServerOperationInterceptors about pre-action call + HookParams hooks = new HookParams() + .add(IBaseResource.class, resourceToDelete) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(TransactionDetails.class, transactionDetails); + doCallHooks(theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks); + + myDeleteConflictService.validateOkToDelete(deleteConflicts, entity, false, theRequest, transactionDetails); + + // Notify interceptors + IdDt idToDelete = entity.getIdDt(); + if (theRequest != null) { + ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete); + notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); + } + + // Perform delete + + updateEntityForDelete(theRequest, transactionDetails, entity); + resourceToDelete.setId(entity.getIdDt()); + + // Notify JPA interceptors + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void beforeCommit(boolean readOnly) { + HookParams hookParams = new HookParams() + .add(IBaseResource.class, resourceToDelete) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(TransactionDetails.class, transactionDetails); + doCallHooks(theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); + } + }); + } + + IBaseOperationOutcome oo; + if (deletedResources.isEmpty()) { + oo = OperationOutcomeUtil.newInstance(getContext()); + String message = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "unableToDeleteNotFound", theUrl); + String severity = "warning"; + String code = "not-found"; + OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); + } else { + oo = OperationOutcomeUtil.newInstance(getContext()); + String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "successfulDeletes", deletedResources.size(), w.getMillis()); + String severity = "information"; + String code = "informational"; + OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); + } + + ourLog.debug("Processed delete on {} (matched {} resource(s)) in {}ms", theUrl, deletedResources.size(), w.getMillis()); + + DeleteMethodOutcome retVal = new DeleteMethodOutcome(); + retVal.setDeletedEntities(deletedResources); + retVal.setOperationOutcome(oo); + return retVal; + } + + private void validateDeleteEnabled() { + if (!myDaoConfig.isDeleteEnabled()) { + String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "deleteBlockedBecauseDisabled"); + throw new PreconditionFailedException(msg); + } + } + + private void validateIdPresentForDelete(IIdType theId) { + if (theId == null || !theId.hasIdPart()) { + throw new InvalidRequestException("Can not perform delete, no ID provided"); + } + } + + @PostConstruct + public void detectSearchDaoDisabled() { + if (mySearchDao != null && mySearchDao.isDisabled()) { + mySearchDao = null; + } + } + + private void doMetaAdd(MT theMetaAdd, BaseHasResource entity) { List tags = toTagList(theMetaAdd); @@ -647,7 +669,6 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override - @Transactional(propagation = Propagation.SUPPORTS) public ExpungeOutcome forceExpungeInExistingTransaction(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); @@ -694,6 +715,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public IBundleProvider history(Date theSince, Date theUntil, RequestDetails theRequestDetails) { // Notify interceptors ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails); @@ -706,10 +728,13 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public IBundleProvider history(final IIdType theId, final Date theSince, Date theUntil, RequestDetails theRequest) { - // Notify interceptors - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId); - notifyInterceptors(RestOperationTypeEnum.HISTORY_INSTANCE, requestDetails); + if (theRequest != null) { + // Notify interceptors + ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId); + notifyInterceptors(RestOperationTypeEnum.HISTORY_INSTANCE, requestDetails); + } StopWatch w = new StopWatch(); @@ -755,6 +780,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) { // Notify interceptors if (theRequest != null) { @@ -787,6 +813,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public MT metaDeleteOperation(IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) { // Notify interceptors if (theRequest != null) { @@ -821,6 +848,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public MT metaGetOperation(Class theType, IIdType theId, RequestDetails theRequest) { // Notify interceptors if (theRequest != null) { @@ -842,6 +870,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public MT metaGetOperation(Class theType, RequestDetails theRequestDetails) { // Notify interceptors if (theRequestDetails != null) { @@ -859,7 +888,10 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) { + return myTransactionService.execute(theRequest, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest)); + } + private DaoMethodOutcome doPatch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) { ResourceTable entityToUpdate; if (isNotBlank(theConditionalUrl)) { @@ -890,10 +922,10 @@ public abstract class BaseHapiFhirResourceDao extends B IBaseResource destination; switch (thePatchType) { case JSON_PATCH: - destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody); + destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody); break; case XML_PATCH: - destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody); + destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody); break; case FHIR_PATCH_XML: case FHIR_PATCH_JSON: @@ -932,6 +964,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public T readByPid(ResourcePersistentId thePid) { StopWatch w = new StopWatch(); @@ -950,16 +983,19 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public T read(IIdType theId) { return read(theId, null); } @Override + @Transactional public T read(IIdType theId, RequestDetails theRequestDetails) { return read(theId, theRequestDetails, false); } @Override + @Transactional public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) { validateResourceTypeAndThrowInvalidRequestException(theId); @@ -1012,11 +1048,13 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) { return readEntity(theId, true, theRequest); } @Override + @Transactional public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) { validateResourceTypeAndThrowInvalidRequestException(theId); @@ -1099,6 +1137,8 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public void reindex(T theResource, ResourceTable theEntity) { + assert TransactionSynchronizationManager.isActualTransactionActive(); + ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getId()); if (theResource != null) { CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE); @@ -1111,11 +1151,13 @@ public abstract class BaseHapiFhirResourceDao extends B } } + @Transactional @Override public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) { removeTag(theId, theTagType, theScheme, theTerm, null); } + @Transactional @Override public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequest) { // Notify interceptors @@ -1216,26 +1258,28 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public Set searchForIds(SearchParameterMap theParams, RequestDetails theRequest) { - theParams.setLoadSynchronousUpTo(10000); + return myTransactionService.execute(theRequest, tx -> { + theParams.setLoadSynchronousUpTo(10000); - ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); + ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); - HashSet retVal = new HashSet<>(); + HashSet retVal = new HashSet<>(); - String uuid = UUID.randomUUID().toString(); - SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); + String uuid = UUID.randomUUID().toString(); + SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); - RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName()); - try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) { - while (iter.hasNext()) { - retVal.add(iter.next()); + RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName()); + try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) { + while (iter.hasNext()) { + retVal.add(iter.next()); + } + } catch (IOException e) { + ourLog.error("IO failure during database access", e); } - } catch (IOException e) { - ourLog.error("IO failure during database access", e); - } - return retVal; + return retVal; + }); } protected MT toMetaDt(Class theType, Collection tagDefinitions) { @@ -1303,10 +1347,21 @@ public abstract class BaseHapiFhirResourceDao extends B String msg = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "missingBody"); throw new InvalidRequestException(msg); } + assert theResource.getIdElement().hasIdPart() || isNotBlank(theMatchUrl); + return myTransactionService.execute(theRequest, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails)); + } + + private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) { StopWatch w = new StopWatch(); - preProcessResourceForStorage(theResource); + T resource = theResource; + if (JpaInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_VERSION_CONFLICT, myInterceptorBroadcaster, theRequest)) { + resource = (T) getContext().getResourceDefinition(theResource).newInstance(); + getContext().newTerser().cloneInto(theResource, resource, false); + } + + preProcessResourceForStorage(resource); final ResourceTable entity; @@ -1321,7 +1376,7 @@ public abstract class BaseHapiFhirResourceDao extends B entity = myEntityManager.find(ResourceTable.class, pid.getId()); resourceId = entity.getIdDt(); } else { - return create(theResource, null, thePerformIndexing, theTransactionDetails, theRequest); + return create(resource, null, thePerformIndexing, theTransactionDetails, theRequest); } } else { /* @@ -1330,13 +1385,15 @@ public abstract class BaseHapiFhirResourceDao extends B * BaseOutcomeReturningMethodBindingWithResourceParam */ resourceId = theResource.getIdElement(); + assert resourceId != null; + assert resourceId.hasIdPart(); RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName()); try { entity = readEntityLatestVersion(resourceId, requestPartitionId); } catch (ResourceNotFoundException e) { requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName()); - return doCreate(theResource, null, thePerformIndexing, theTransactionDetails, theRequest, requestPartitionId); + return doCreateForPostOrPut(resource, null, thePerformIndexing, theTransactionDetails, theRequest, requestPartitionId); } } @@ -1372,8 +1429,8 @@ public abstract class BaseHapiFhirResourceDao extends B * directly. So we just bail now. */ if (!thePerformIndexing) { - theResource.setId(entity.getIdDt().getValue()); - DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(wasDeleted); + resource.setId(entity.getIdDt().getValue()); + DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, resource).setCreated(wasDeleted); outcome.setPreviousResource(oldResource); return outcome; } @@ -1381,11 +1438,13 @@ public abstract class BaseHapiFhirResourceDao extends B /* * Otherwise, we're not in a transaction */ - ResourceTable savedEntity = updateInternal(theRequest, theResource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource, theTransactionDetails); - DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, theResource).setCreated(wasDeleted); + ResourceTable savedEntity = updateInternal(theRequest, resource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource, theTransactionDetails); + DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, resource).setCreated(wasDeleted); if (!thePerformIndexing) { - outcome.setId(theResource.getIdElement()); + IIdType id = getContext().getVersion().newIdType(); + id.setValue(entity.getIdDt().getValue()); + outcome.setId(id); } String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirResourceDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart()); 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 60111e1b662..afbfdc0c954 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 @@ -32,6 +32,7 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.DeleteConflict; import ca.uhn.fhir.jpa.api.model.DeleteConflictList; import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; import ca.uhn.fhir.jpa.model.entity.ResourceTable; @@ -64,7 +65,6 @@ import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.UrlUtil; -import com.google.common.base.Charsets; import com.google.common.collect.ArrayListMultimap; import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu3.model.Bundle; @@ -83,6 +83,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.PostConstruct; @@ -121,6 +122,8 @@ public abstract class BaseTransactionProcessor { private IInterceptorBroadcaster myInterceptorBroadcaster; @Autowired private MatchResourceUrlService myMatchResourceUrlService; + @Autowired + private HapiTransactionService myHapiTransactionService; @PostConstruct public void start() { @@ -344,9 +347,6 @@ public abstract class BaseTransactionProcessor { final TransactionDetails transactionDetails = new TransactionDetails(); final StopWatch transactionStopWatch = new StopWatch(); - final Set allIds = new LinkedHashSet<>(); - final Map idSubstitutions = new HashMap<>(); - final Map idToPersistedOutcome = new HashMap<>(); List requestEntries = myVersionAdapter.getEntries(theRequest); // Do all entries have a verb? @@ -403,13 +403,16 @@ public abstract class BaseTransactionProcessor { * heavy load with lots of concurrent transactions using all available * database connections. */ - TransactionTemplate txManager = new TransactionTemplate(myTxManager); - Map entriesToProcess = txManager.execute(status -> { + TransactionCallback> txCallback = status -> { + final Set allIds = new LinkedHashSet<>(); + final Map idSubstitutions = new HashMap<>(); + final Map idToPersistedOutcome = new HashMap<>(); Map retVal = doTransactionWriteOperations(theRequestDetails, theActionName, transactionDetails, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries, transactionStopWatch); transactionStopWatch.startTask("Commit writes to database"); return retVal; - }); + }; + Map entriesToProcess = myHapiTransactionService.execute(theRequestDetails, txCallback); transactionStopWatch.endCurrentTask(); for (Map.Entry nextEntry : entriesToProcess.entrySet()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java index 123b812efa6..ec2b7bd27e6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java @@ -57,6 +57,8 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import static ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoValueSetDstu3.vsValidateCodeOptions; +import static ca.uhn.fhir.jpa.util.LogicUtil.multiXor; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -291,102 +293,14 @@ public class FhirResourceDaoValueSetDstu2 extends BaseHapiFhirResourceDao thePrimitive) { + public static String toStringOrNull(IPrimitiveType thePrimitive) { return thePrimitive != null ? thePrimitive.getValue() : null; } @Override - public ValidateCodeResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, + public IValidationSupport.CodeValidationResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, IPrimitiveType theSystem, IPrimitiveType theDisplay, CodingDt theCoding, CodeableConceptDt theCodeableConcept, RequestDetails theRequest) { - List valueSetIds; - - boolean haveCodeableConcept = theCodeableConcept != null && theCodeableConcept.getCoding().size() > 0; - boolean haveCoding = theCoding != null && theCoding.isEmpty() == false; - boolean haveCode = theCode != null && theCode.isEmpty() == false; - - if (!haveCodeableConcept && !haveCoding && !haveCode) { - throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); - } - if (!multiXor(haveCodeableConcept, haveCoding, haveCode)) { - throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); - } - - boolean haveIdentifierParam = theValueSetIdentifier != null && theValueSetIdentifier.isEmpty() == false; - if (theId != null) { - valueSetIds = Collections.singletonList(theId); - } else if (haveIdentifierParam) { - Set ids = searchForIds(new SearchParameterMap(ValueSet.SP_IDENTIFIER, new TokenParam(null, theValueSetIdentifier.getValue())), theRequest); - valueSetIds = new ArrayList<>(); - for (ResourcePersistentId next : ids) { - IIdType id = myIdHelperService.translatePidIdToForcedId(myFhirContext, "ValueSet", next); - valueSetIds.add(id); - } - } else { - if (theCode == null || theCode.isEmpty()) { - throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); - } - String code = theCode.getValue(); - String system = toStringOrNull(theSystem); - valueSetIds = findCodeSystemIdsContainingSystemAndCode(code, system, theRequest); - } - - for (IIdType nextId : valueSetIds) { - ValueSet expansion = expand(nextId, null, theRequest); - List contains = expansion.getExpansion().getContains(); - ValidateCodeResult result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept); - if (result != null) { - if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) { - if (!theDisplay.getValue().equals(result.getDisplay())) { - return new ValidateCodeResult(false, "Display for code does not match", result.getDisplay()); - } - } - return result; - } - } - - return new ValidateCodeResult(false, "Code not found", null); - } - - private ValidateCodeResult validateCodeIsInContains(List contains, String theSystem, String theCode, CodingDt theCoding, - CodeableConceptDt theCodeableConcept) { - for (ExpansionContains nextCode : contains) { - ValidateCodeResult result = validateCodeIsInContains(nextCode.getContains(), theSystem, theCode, theCoding, theCodeableConcept); - if (result != null) { - return result; - } - - String system = nextCode.getSystem(); - String code = nextCode.getCode(); - - if (isNotBlank(theCode)) { - if (theCode.equals(code) && (isBlank(theSystem) || theSystem.equals(system))) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else if (theCoding != null) { - if (StringUtils.equals(system, theCoding.getSystem()) && StringUtils.equals(code, theCoding.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else { - for (CodingDt next : theCodeableConcept.getCoding()) { - if (StringUtils.equals(system, next.getSystem()) && StringUtils.equals(code, next.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } - } - - } - - return null; - } - - private static boolean multiXor(boolean... theValues) { - int count = 0; - for (int i = 0; i < theValues.length; i++) { - if (theValues[i]) { - count++; - } - } - return count == 1; + return myTerminologySvc.validateCode(vsValidateCodeOptions(), theId, toStringOrNull(theValueSetIdentifier), toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 38313fc1c86..f31d9f588b6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.DeleteConflictList; import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; @@ -73,7 +74,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -109,14 +109,13 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { private DaoRegistry myDaoRegistry; @Autowired private MatchResourceUrlService myMatchResourceUrlService; + @Autowired + private HapiTransactionService myHapiTransactionalService; private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); long start = System.currentTimeMillis(); - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - Bundle resp = new Bundle(); resp.setType(BundleTypeEnum.BATCH_RESPONSE); @@ -144,7 +143,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { // create their own nextResponseBundle = callback.doInTransaction(null); } else { - nextResponseBundle = txTemplate.execute(callback); + nextResponseBundle = myHapiTransactionalService.execute(theRequestDetails, callback); } caughtEx = null; 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 4b55f0edbef..7ac35fef7c0 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 @@ -90,6 +90,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.annotation.Nonnull; import javax.persistence.EntityManager; @@ -245,6 +246,7 @@ public class SearchBuilder implements ISearchBuilder { @Override public Iterator createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId) { assert theRequestPartitionId != null; + assert TransactionSynchronizationManager.isActualTransactionActive(); init(theParams, theSearchUuid, theRequestPartitionId); @@ -263,6 +265,7 @@ public class SearchBuilder implements ISearchBuilder { @Override public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId) { assert theRequestPartitionId != null; + assert TransactionSynchronizationManager.isActualTransactionActive(); init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java index 8648586074b..9e6e229c04f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; * #L% */ +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; @@ -29,14 +30,12 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; import ca.uhn.fhir.jpa.model.entity.ResourceTable; -import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; -import ca.uhn.fhir.jpa.util.LogicUtil; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.ElementUtil; -import org.apache.commons.codec.binary.StringUtils; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; @@ -50,15 +49,14 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.utilities.validation.ValidationOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Collections; import java.util.Date; import java.util.List; +import static ca.uhn.fhir.jpa.dao.FhirResourceDaoValueSetDstu2.toStringOrNull; import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoValueSetR4.validateHaveExpansionOrThrowInternalErrorException; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -81,7 +79,7 @@ public class FhirResourceDaoValueSetDstu3 extends BaseHapiFhirResourceDao theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, - IPrimitiveType theSystem, IPrimitiveType theDisplay, Coding theCoding, - CodeableConcept theCodeableConcept, RequestDetails theRequestDetails) { - - List valueSetIds = Collections.emptyList(); - - boolean haveCodeableConcept = theCodeableConcept != null && theCodeableConcept.getCoding().size() > 0; - boolean haveCoding = theCoding != null && theCoding.isEmpty() == false; - boolean haveCode = theCode != null && theCode.isEmpty() == false; - - if (!haveCodeableConcept && !haveCoding && !haveCode) { - throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); - } - if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { - throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); - } - - boolean haveIdentifierParam = theValueSetIdentifier != null && theValueSetIdentifier.isEmpty() == false; - ValueSet vs = null; - boolean isBuiltInValueSet = false; - if (theId != null) { - vs = read(theId, theRequestDetails); - } else if (haveIdentifierParam) { - vs = (ValueSet) myDefaultProfileValidationSupport.fetchValueSet(theValueSetIdentifier.getValue()); - if (vs == null) { - vs = (ValueSet) myValidationSupport.fetchValueSet(theValueSetIdentifier.getValue()); - if (vs == null) { - throw new InvalidRequestException("Unknown ValueSet identifier: " + theValueSetIdentifier.getValue()); - } - } else { - isBuiltInValueSet = true; - } - } else { - if (theCode == null || theCode.isEmpty()) { - throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); - } - // String code = theCode.getValue(); - // String system = toStringOrNull(theSystem); - IValidationSupport.LookupCodeResult result = myCodeSystemDao.lookupCode(theCode, theSystem, null, null); - if (result != null && result.isFound()) { - IFhirResourceDaoValueSet.ValidateCodeResult retVal = new ValidateCodeResult(true, "Found code", result.getCodeDisplay()); - return retVal; - } - } - - if (vs != null) { - ValidateCodeResult result; - if (myDaoConfig.isPreExpandValueSets() && !isBuiltInValueSet && myTerminologySvc.isValueSetPreExpandedForCodeValidation(vs)) { - result = myTerminologySvc.validateCodeIsInPreExpandedValueSet(new ValidationOptions(), vs, toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); - } else { - ValueSet expansion = doExpand(vs); - List contains = expansion.getExpansion().getContains(); - result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept); - } - if (result != null) { - if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) { - if (!theDisplay.getValue().equals(result.getDisplay())) { - return new ValidateCodeResult(false, "Display for code does not match", result.getDisplay()); - } - } - return result; - } - } - - return new ValidateCodeResult(false, "Code not found", null); - - } - - private String toStringOrNull(IPrimitiveType thePrimitive) { - return thePrimitive != null ? thePrimitive.getValue() : null; - } - - private IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInContains(List contains, String theSystem, String theCode, - Coding theCoding, CodeableConcept theCodeableConcept) { - for (ValueSetExpansionContainsComponent nextCode : contains) { - IFhirResourceDaoValueSet.ValidateCodeResult result = validateCodeIsInContains(nextCode.getContains(), theSystem, theCode, theCoding, theCodeableConcept); - if (result != null) { - return result; - } - - String system = nextCode.getSystem(); - String code = nextCode.getCode(); - - if (isNotBlank(theCode)) { - if (theCode.equals(code) && (isBlank(theSystem) || theSystem.equals(system))) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else if (theCoding != null) { - if (StringUtils.equals(system, theCoding.getSystem()) && StringUtils.equals(code, theCoding.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else { - for (Coding next : theCodeableConcept.getCoding()) { - if (StringUtils.equals(system, next.getSystem()) && StringUtils.equals(code, next.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } - } - - } - - return null; + public IValidationSupport.CodeValidationResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, + IPrimitiveType theSystem, IPrimitiveType theDisplay, Coding theCoding, + CodeableConcept theCodeableConcept, RequestDetails theRequestDetails) { + return myTerminologySvc.validateCode(vsValidateCodeOptions(), theId, toStringOrNull(theValueSetIdentifier), toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); } @Override @@ -395,4 +295,8 @@ public class FhirResourceDaoValueSetDstu3 extends BaseHapiFhirResourceDao } @Override - public ValidateCodeResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, - IPrimitiveType theSystem, IPrimitiveType theDisplay, Coding theCoding, - CodeableConcept theCodeableConcept, RequestDetails theRequestDetails) { + public IValidationSupport.CodeValidationResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, + IPrimitiveType theSystem, IPrimitiveType theDisplay, Coding theCoding, + CodeableConcept theCodeableConcept, RequestDetails theRequestDetails) { - List valueSetIds = Collections.emptyList(); - - boolean haveCodeableConcept = theCodeableConcept != null && theCodeableConcept.getCoding().size() > 0; - boolean haveCoding = theCoding != null && !theCoding.isEmpty(); - boolean haveCode = theCode != null && !theCode.isEmpty(); - - if (!haveCodeableConcept && !haveCoding && !haveCode) { - throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); - } - if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { - throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); - } - - boolean haveIdentifierParam = theValueSetIdentifier != null && !theValueSetIdentifier.isEmpty(); - ValueSet vs = null; - boolean isBuiltInValueSet = false; - if (theId != null) { - vs = read(theId, theRequestDetails); - } else if (haveIdentifierParam) { - vs = (ValueSet) myDefaultProfileValidationSupport.fetchValueSet(theValueSetIdentifier.getValue()); - if (vs == null) { - vs = (ValueSet) myValidationSupport.fetchValueSet(theValueSetIdentifier.getValue()); - if (vs == null) { - throw new InvalidRequestException("Unknown ValueSet identifier: " + theValueSetIdentifier.getValue()); - } - } else { - isBuiltInValueSet = true; - } - } else { - if (theCode == null || theCode.isEmpty()) { - throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); - } - // String code = theCode.getValue(); - // String system = toStringOrNull(theSystem); - IValidationSupport.LookupCodeResult result = myCodeSystemDao.lookupCode(theCode, theSystem, null, null); - if (result.isFound()) { - ValidateCodeResult retVal = new ValidateCodeResult(true, "Found code", result.getCodeDisplay()); - return retVal; - } - } - - if (vs != null) { - ValidateCodeResult result; - if (myDaoConfig.isPreExpandValueSets() && !isBuiltInValueSet && myTerminologySvc.isValueSetPreExpandedForCodeValidation(vs)) { - result = myTerminologySvc.validateCodeIsInPreExpandedValueSet(new ValidationOptions(), vs, toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); - } else { - ValueSet expansion = doExpand(vs); - List contains = expansion.getExpansion().getContains(); - result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept); - } - if (result != null) { - if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) { - if (!theDisplay.getValue().equals(result.getDisplay())) { - return new ValidateCodeResult(false, "Display for code does not match", result.getDisplay()); - } - } - return result; - } - } - - return new ValidateCodeResult(false, "Code not found", null); - - } - - private String toStringOrNull(IPrimitiveType thePrimitive) { - return thePrimitive != null ? thePrimitive.getValue() : null; - } - - private ValidateCodeResult validateCodeIsInContains(List contains, String theSystem, String theCode, - Coding theCoding, CodeableConcept theCodeableConcept) { - for (ValueSetExpansionContainsComponent nextCode : contains) { - ValidateCodeResult result = validateCodeIsInContains(nextCode.getContains(), theSystem, theCode, theCoding, theCodeableConcept); - if (result != null) { - return result; - } - - String system = nextCode.getSystem(); - String code = nextCode.getCode(); - - if (isNotBlank(theCode)) { - if (theCode.equals(code) && (isBlank(theSystem) || theSystem.equals(system))) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else if (theCoding != null) { - if (StringUtils.equals(system, theCoding.getSystem()) && StringUtils.equals(code, theCoding.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else { - for (Coding next : theCodeableConcept.getCoding()) { - if (StringUtils.equals(system, next.getSystem()) && StringUtils.equals(code, next.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } - } - - } - - return null; + return myTerminologySvc.validateCode(vsValidateCodeOptions(), theId, toStringOrNull(theValueSetIdentifier), toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoValueSetR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoValueSetR5.java index a015d462181..5001ca5420a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoValueSetR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoValueSetR5.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.dao.r5; * #L% */ +import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; @@ -55,6 +56,8 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import static ca.uhn.fhir.jpa.dao.FhirResourceDaoValueSetDstu2.toStringOrNull; +import static ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoValueSetDstu3.vsValidateCodeOptions; import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoValueSetR4.validateHaveExpansionOrThrowInternalErrorException; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -245,108 +248,10 @@ public class FhirResourceDaoValueSetR5 extends BaseHapiFhirResourceDao } @Override - public ValidateCodeResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, - IPrimitiveType theSystem, IPrimitiveType theDisplay, Coding theCoding, - CodeableConcept theCodeableConcept, RequestDetails theRequestDetails) { - - List valueSetIds = Collections.emptyList(); - - boolean haveCodeableConcept = theCodeableConcept != null && theCodeableConcept.getCoding().size() > 0; - boolean haveCoding = theCoding != null && theCoding.isEmpty() == false; - boolean haveCode = theCode != null && theCode.isEmpty() == false; - - if (!haveCodeableConcept && !haveCoding && !haveCode) { - throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); - } - if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { - throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); - } - - boolean haveIdentifierParam = theValueSetIdentifier != null && theValueSetIdentifier.isEmpty() == false; - ValueSet vs = null; - boolean isBuiltInValueSet = false; - if (theId != null) { - vs = read(theId, theRequestDetails); - } else if (haveIdentifierParam) { - vs = (ValueSet) myDefaultProfileValidationSupport.fetchValueSet(theValueSetIdentifier.getValue()); - if (vs == null) { - vs = (ValueSet) myValidationSupport.fetchValueSet(theValueSetIdentifier.getValue()); - if (vs == null) { - throw new InvalidRequestException("Unknown ValueSet identifier: " + theValueSetIdentifier.getValue()); - } - } else { - isBuiltInValueSet = true; - } - } else { - if (theCode == null || theCode.isEmpty()) { - throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); - } - // String code = theCode.getValue(); - // String system = toStringOrNull(theSystem); - IValidationSupport.LookupCodeResult result = myCodeSystemDao.lookupCode(theCode, theSystem, null, null); - if (result.isFound()) { - ValidateCodeResult retVal = new ValidateCodeResult(true, "Found code", result.getCodeDisplay()); - return retVal; - } - } - - if (vs != null) { - ValidateCodeResult result; - if (myDaoConfig.isPreExpandValueSets() && !isBuiltInValueSet && myTerminologySvc.isValueSetPreExpandedForCodeValidation(vs)) { - result = myTerminologySvc.validateCodeIsInPreExpandedValueSet(new ValidationOptions(), vs, toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); - } else { - ValueSet expansion = doExpand(vs); - List contains = expansion.getExpansion().getContains(); - result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept); - } - if (result != null) { - if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) { - if (!theDisplay.getValue().equals(result.getDisplay())) { - return new ValidateCodeResult(false, "Display for code does not match", result.getDisplay()); - } - } - return result; - } - } - - return new ValidateCodeResult(false, "Code not found", null); - - } - - private String toStringOrNull(IPrimitiveType thePrimitive) { - return thePrimitive != null ? thePrimitive.getValue() : null; - } - - private ValidateCodeResult validateCodeIsInContains(List contains, String theSystem, String theCode, - Coding theCoding, CodeableConcept theCodeableConcept) { - for (ValueSetExpansionContainsComponent nextCode : contains) { - ValidateCodeResult result = validateCodeIsInContains(nextCode.getContains(), theSystem, theCode, theCoding, theCodeableConcept); - if (result != null) { - return result; - } - - String system = nextCode.getSystem(); - String code = nextCode.getCode(); - - if (isNotBlank(theCode)) { - if (theCode.equals(code) && (isBlank(theSystem) || theSystem.equals(system))) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else if (theCoding != null) { - if (StringUtils.equals(system, theCoding.getSystem()) && StringUtils.equals(code, theCoding.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } else { - for (Coding next : theCodeableConcept.getCoding()) { - if (StringUtils.equals(system, next.getSystem()) && StringUtils.equals(code, next.getCode())) { - return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay()); - } - } - } - - } - - return null; + public IValidationSupport.CodeValidationResult validateCode(IPrimitiveType theValueSetIdentifier, IIdType theId, IPrimitiveType theCode, + IPrimitiveType theSystem, IPrimitiveType theDisplay, Coding theCoding, + CodeableConcept theCodeableConcept, RequestDetails theRequestDetails) { + return myTerminologySvc.validateCode(vsValidateCodeOptions(), theId, toStringOrNull(theValueSetIdentifier), toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java new file mode 100644 index 00000000000..b6565e8e12a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java @@ -0,0 +1,103 @@ +package ca.uhn.fhir.jpa.dao.tx; + +/*- + * #%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.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy; +import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.PostConstruct; + +public class HapiTransactionService { + + private static final Logger ourLog = LoggerFactory.getLogger(HapiTransactionService.class); + @Autowired + private IInterceptorBroadcaster myInterceptorBroadcaster; + @Autowired + private PlatformTransactionManager myTransactionManager; + private TransactionTemplate myTxTemplate; + + @PostConstruct + public void start() { + myTxTemplate = new TransactionTemplate(myTransactionManager); + } + + public T execute(RequestDetails theRequestDetails, TransactionCallback theCallback) { + + for (int i = 0; ; i++) { + try { + + try { + return myTxTemplate.execute(theCallback); + } catch (MyException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new InternalErrorException(e); + } + } + + } catch (ResourceVersionConflictException e) { + ourLog.debug("Version conflict detected: {}", e.toString()); + + HookParams params = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); + ResourceVersionConflictResolutionStrategy conflictResolutionStrategy = (ResourceVersionConflictResolutionStrategy) JpaInterceptorBroadcaster.doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_VERSION_CONFLICT, params); + if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) { + if (i <= conflictResolutionStrategy.getMaxRetries()) { + continue; + } + + ourLog.info("Max retries ({}) exceeded for version conflict", conflictResolutionStrategy.getMaxRetries()); + } + + throw e; + } + } + + + } + + /** + * This is just an unchecked exception so that we can catch checked exceptions inside TransactionTemplate + * and rethrow them outside of it + */ + static class MyException extends RuntimeException { + + public MyException(Throwable theThrowable) { + super(theThrowable); + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactional.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactional.java new file mode 100644 index 00000000000..81d0cc19d99 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactional.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.jpa.dao.tx; + +/*- + * #%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.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @see HapiTransactionalAspect + * @since 5.1.0 + */ +@Retention(RUNTIME) +@Target({METHOD, TYPE}) +public @interface HapiTransactional { +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java index 23b8ce70b1a..f7f1fdbc687 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java @@ -44,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.List; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java index b422ad16233..27f6b8c2dfd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java @@ -25,15 +25,24 @@ import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.DateOrListParam; import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.NumberOrListParam; import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.QuantityOrListParam; import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.SpecialOrListParam; import ca.uhn.fhir.rest.param.SpecialParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; @@ -56,6 +65,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import java.util.stream.Stream; import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER; @@ -111,42 +121,63 @@ public class JpaStorageServices extends BaseHapiFhirDao implement throw new InvalidRequestException(msg); } - for (Value nextValue : nextArgument.getValues()) { - String value = nextValue.getValue(); + IQueryParameterOr queryParam; - IQueryParameterType param = null; - switch (searchParam.getParamType()) { - case NUMBER: - param = new NumberParam(value); - break; - case DATE: - param = new DateParam(value); - break; - case STRING: - param = new StringParam(value); - break; - case TOKEN: - param = new TokenParam(null, value); - break; - case REFERENCE: - param = new ReferenceParam(value); - break; - case COMPOSITE: - throw new InvalidRequestException("Composite parameters are not yet supported in GraphQL"); - case QUANTITY: - param = new QuantityParam(value); - break; - case SPECIAL: - param = new SpecialParam().setValue(value); - break; - case URI: - break; - case HAS: - break; - } - - params.add(searchParamName, param); + switch (searchParam.getParamType()) { + case NUMBER: + NumberOrListParam numberOrListParam = new NumberOrListParam(); + for (Value value: nextArgument.getValues()) { + numberOrListParam.addOr(new NumberParam(value.getValue())); + } + queryParam = numberOrListParam; + break; + case DATE: + DateOrListParam dateOrListParam = new DateOrListParam(); + for (Value value: nextArgument.getValues()) { + dateOrListParam.addOr(new DateParam(value.getValue())); + } + queryParam = dateOrListParam; + break; + case STRING: + StringOrListParam stringOrListParam = new StringOrListParam(); + for (Value value: nextArgument.getValues()) { + stringOrListParam.addOr(new StringParam(value.getValue())); + } + queryParam = stringOrListParam; + break; + case TOKEN: + TokenOrListParam tokenOrListParam = new TokenOrListParam(); + for (Value value: nextArgument.getValues()) { + tokenOrListParam.addOr(new TokenParam(value.getValue())); + } + queryParam = tokenOrListParam; + break; + case REFERENCE: + ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam(); + for (Value value: nextArgument.getValues()) { + referenceOrListParam.addOr(new ReferenceParam(value.getValue())); + } + queryParam = referenceOrListParam; + break; + case QUANTITY: + QuantityOrListParam quantityOrListParam = new QuantityOrListParam(); + for (Value value: nextArgument.getValues()) { + quantityOrListParam.addOr(new QuantityParam(value.getValue())); + } + queryParam = quantityOrListParam; + break; + case SPECIAL: + SpecialOrListParam specialOrListParam = new SpecialOrListParam(); + for (Value value: nextArgument.getValues()) { + specialOrListParam.addOr(new SpecialParam().setValue(value.getValue())); + } + queryParam = specialOrListParam; + break; + default: + throw new InvalidRequestException(String.format("%s parameters are not yet supported in GraphQL", searchParam.getParamType())); } + + params.add(searchParamName, queryParam); } RequestDetails requestDetails = (RequestDetails) theAppInfo; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/UserRequestRetryVersionConflictsInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/UserRequestRetryVersionConflictsInterceptor.java new file mode 100644 index 00000000000..ed2a31a24a9 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/UserRequestRetryVersionConflictsInterceptor.java @@ -0,0 +1,82 @@ +package ca.uhn.fhir.jpa.interceptor; + +/*- + * #%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.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy; +import ca.uhn.fhir.rest.api.server.RequestDetails; + +import java.util.StringTokenizer; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.trim; + +/** + * This interceptor looks for a header on incoming requests called X-Retry-On-Version-Conflict and + * if present, it will instruct the server to automatically retry JPA server operations that would have + * otherwise failed with a {@link ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException} (HTTP 409). + *

+ * The format of the header is:
+ * X-Retry-On-Version-Conflict: retry; max-retries=100 + *

+ */ +@Interceptor +public class UserRequestRetryVersionConflictsInterceptor { + + public static final String HEADER_NAME = "X-Retry-On-Version-Conflict"; + public static final String MAX_RETRIES = "max-retries"; + public static final String RETRY = "retry"; + + @Hook(value = Pointcut.STORAGE_VERSION_CONFLICT, order = 100) + public ResourceVersionConflictResolutionStrategy check(RequestDetails theRequestDetails) { + ResourceVersionConflictResolutionStrategy retVal = new ResourceVersionConflictResolutionStrategy(); + + if (theRequestDetails != null) { + for (String headerValue : theRequestDetails.getHeaders(HEADER_NAME)) { + if (isNotBlank(headerValue)) { + + StringTokenizer tok = new StringTokenizer(headerValue, ";"); + while (tok.hasMoreTokens()) { + String next = trim(tok.nextToken()); + if (next.equals(RETRY)) { + retVal.setRetry(true); + } else if (next.startsWith(MAX_RETRIES + "=")) { + + String val = trim(next.substring((MAX_RETRIES + "=").length())); + int maxRetries = Integer.parseInt(val); + maxRetries = Math.min(100, maxRetries); + retVal.setMaxRetries(maxRetries); + + } + + } + + } + } + } + + return retVal; + } + + +} 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 index e209831e7b3..a2da265c559 100644 --- 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 @@ -297,7 +297,8 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac } myPackageVersionResourceDao.save(resourceEntity); - String msg = "Indexing Resource[" + dirName + '/' + nextFile + "] with URL: " + defaultString(url) + "|" + defaultString(version); + String resType = packageContext.getResourceType(resource); + String msg = "Indexing " + resType + " Resource[" + dirName + '/' + nextFile + "] with URL: " + defaultString(url) + "|" + defaultString(version); getProcessingMessages(npmPackage).add(msg); ourLog.info("Package[{}#{}] " + msg, thePackageId, packageVersionId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java index 5c00c9faaf7..280de0f26ec 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java @@ -31,6 +31,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; 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.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.FhirTerser; import com.google.common.collect.Lists; import com.google.gson.Gson; @@ -258,13 +259,14 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc { return Collections.EMPTY_LIST; } ArrayList resources = new ArrayList<>(); - for (String file : pkg.getFolders().get("package").listFiles()) { - if (file.contains(type)) { + List filesForType = pkg.getFolders().get("package").getTypes().get(type); + if (filesForType != null) { + for (String file : filesForType) { try { byte[] content = pkg.getFolders().get("package").fetchFile(file); resources.add(fhirContext.newJsonParser().parseResource(new String(content))); } catch (IOException e) { - ourLog.error("Cannot install resource of type {}: Could not fetch file {}", type, file); + throw new InternalErrorException("Cannot install resource of type "+type+": Could not fetch file "+ file, e); } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index ba331d911c8..a8897ba6aa0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -35,9 +35,11 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.transaction.Transactional; import java.util.HashSet; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooks; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java index 3daed2b4d35..53cc58213f8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderValueSetDstu2.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.provider; * #L% */ +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; @@ -39,6 +40,8 @@ import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBaseParameters; import javax.servlet.http.HttpServletRequest; @@ -142,21 +145,27 @@ public class BaseJpaResourceProviderValueSetDstu2 extends JpaResourceProviderDst startRequest(theServletRequest); try { IFhirResourceDaoValueSet dao = (IFhirResourceDaoValueSet) getDao(); - IFhirResourceDaoValueSet.ValidateCodeResult result = dao.validateCode(theValueSetIdentifier, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); - Parameters retVal = new Parameters(); - retVal.addParameter().setName("result").setValue(new BooleanDt(result.isResult())); - if (isNotBlank(result.getMessage())) { - retVal.addParameter().setName("message").setValue(new StringDt(result.getMessage())); - } - if (isNotBlank(result.getDisplay())) { - retVal.addParameter().setName("display").setValue(new StringDt(result.getDisplay())); - } - return retVal; + IValidationSupport.CodeValidationResult result = dao.validateCode(theValueSetIdentifier, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); + return (Parameters) toValidateCodeResult(getContext(), result); } finally { endRequest(theServletRequest); } } + public static IBaseParameters toValidateCodeResult(FhirContext theContext, IValidationSupport.CodeValidationResult theResult) { + IBaseParameters retVal = ParametersUtil.newInstance(theContext); + + ParametersUtil.addParameterToParametersBoolean(theContext, retVal, "result", theResult.isOk()); + if (isNotBlank(theResult.getMessage())) { + ParametersUtil.addParameterToParametersString(theContext, retVal, "message", theResult.getMessage()); + } + if (isNotBlank(theResult.getDisplay())) { + ParametersUtil.addParameterToParametersString(theContext, retVal, "display", theResult.getDisplay()); + } + + return retVal; + } + private static boolean moreThanOneTrue(boolean... theBooleans) { boolean haveOne = false; for (boolean next : theBooleans) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java index 4c668129f13..b0c1ab30e97 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java @@ -25,9 +25,12 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Initialize; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -107,9 +110,23 @@ public class GraphQLProvider { myStorageServices = theStorageServices; } - @GraphQL - public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { + @GraphQL(type=RequestTypeEnum.GET) + public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String queryUrl) { + if (queryUrl != null) { + return processGraphQLRequest(theRequestDetails, theId, queryUrl); + } + throw new InvalidRequestException("Unable to parse empty GraphQL expression"); + } + @GraphQL(type=RequestTypeEnum.POST) + public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String queryBody) { + if (queryBody != null) { + return processGraphQLRequest(theRequestDetails, theId, queryBody); + } + throw new InvalidRequestException("Unable to parse empty GraphQL expression"); + } + + public String processGraphQLRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQuery) { IGraphQLEngine engine = engineFactory.get(); engine.setAppInfo(theRequestDetails); engine.setServices(myStorageServices); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java index b1d3d521438..bd394f64027 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderValueSetDstu3.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.provider.dstu3; * #L% */ +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.provider.BaseJpaResourceProviderValueSetDstu2; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -148,16 +150,8 @@ public class BaseJpaResourceProviderValueSetDstu3 extends JpaResourceProviderDst startRequest(theServletRequest); try { IFhirResourceDaoValueSet dao = (IFhirResourceDaoValueSet) getDao(); - IFhirResourceDaoValueSet.ValidateCodeResult result = dao.validateCode(url, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); - Parameters retVal = new Parameters(); - retVal.addParameter().setName("result").setValue(new BooleanType(result.isResult())); - if (isNotBlank(result.getMessage())) { - retVal.addParameter().setName("message").setValue(new StringType(result.getMessage())); - } - if (isNotBlank(result.getDisplay())) { - retVal.addParameter().setName("display").setValue(new StringType(result.getDisplay())); - } - return retVal; + IValidationSupport.CodeValidationResult result = dao.validateCode(url, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); + return (Parameters) BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result); } finally { endRequest(theServletRequest); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java index 640de98b412..c8d4890cf04 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderValueSetR4.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.provider.r4; * #L% */ +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.provider.BaseJpaResourceProviderValueSetDstu2; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -134,16 +136,8 @@ public class BaseJpaResourceProviderValueSetR4 extends JpaResourceProviderR4 dao = (IFhirResourceDaoValueSet) getDao(); - IFhirResourceDaoValueSet.ValidateCodeResult result = dao.validateCode(theValueSetUrl, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); - Parameters retVal = new Parameters(); - retVal.addParameter().setName("result").setValue(new BooleanType(result.isResult())); - if (isNotBlank(result.getMessage())) { - retVal.addParameter().setName("message").setValue(new StringType(result.getMessage())); - } - if (isNotBlank(result.getDisplay())) { - retVal.addParameter().setName("display").setValue(new StringType(result.getDisplay())); - } - return retVal; + IValidationSupport.CodeValidationResult result = dao.validateCode(theValueSetUrl, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); + return (Parameters) BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result); } finally { endRequest(theServletRequest); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderValueSetR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderValueSetR5.java index a04b8c91374..244839c8ae3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderValueSetR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/BaseJpaResourceProviderValueSetR5.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.provider.r5; * #L% */ +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.provider.BaseJpaResourceProviderValueSetDstu2; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -134,16 +136,8 @@ public class BaseJpaResourceProviderValueSetR5 extends JpaResourceProviderR5 dao = (IFhirResourceDaoValueSet) getDao(); - IFhirResourceDaoValueSet.ValidateCodeResult result = dao.validateCode(theValueSetUrl, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); - Parameters retVal = new Parameters(); - retVal.addParameter().setName("result").setValue(new BooleanType(result.isResult())); - if (isNotBlank(result.getMessage())) { - retVal.addParameter().setName("message").setValue(new StringType(result.getMessage())); - } - if (isNotBlank(result.getDisplay())) { - retVal.addParameter().setName("display").setValue(new StringType(result.getDisplay())); - } - return retVal; + IValidationSupport.CodeValidationResult result = dao.validateCode(theValueSetUrl, theId, theCode, theSystem, theDisplay, theCoding, theCodeableConcept, theRequestDetails); + return (Parameters) BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result); } finally { endRequest(theServletRequest); } 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 7e49d60dd5b..023d1c57eaa 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 @@ -30,7 +30,6 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.model.TranslationQuery; import ca.uhn.fhir.jpa.api.model.TranslationRequest; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; @@ -73,6 +72,7 @@ import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException; +import ca.uhn.fhir.jpa.util.LogicUtil; import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; @@ -81,6 +81,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.ValidateUtil; @@ -105,10 +106,14 @@ import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.FullTextQuery; import org.hibernate.search.query.dsl.BooleanJunction; import org.hibernate.search.query.dsl.QueryBuilder; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeSystem; @@ -169,6 +174,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNoneBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; public abstract class BaseTermReadSvcImpl implements ITermReadSvc { public static final int DEFAULT_FETCH_SIZE = 250; @@ -653,16 +659,21 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { includedConcepts = theIncludeOrExclude .getConcept() .stream() - .map(t->new VersionIndependentConcept(theIncludeOrExclude.getSystem(), t.getCode())) + .map(t -> new VersionIndependentConcept(theIncludeOrExclude.getSystem(), t.getCode())) .collect(Collectors.toList()); } if (includedConcepts != null) { int foundCount = 0; for (VersionIndependentConcept next : includedConcepts) { - LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(myValidationSupport), next.getSystem(), next.getCode()); + String nextSystem = next.getSystem(); + if (nextSystem == null) { + nextSystem = system; + } + + LookupCodeResult lookup = myValidationSupport.lookupCode(new ValidationSupportContext(provideValidationSupport()), nextSystem, next.getCode()); if (lookup != null && lookup.isFound()) { - addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, next.getSystem(), next.getCode(), lookup.getCodeDisplay()); + addOrRemoveCode(theValueSetCodeAccumulator, theAddedCodes, theAdd, nextSystem, next.getCode(), lookup.getCodeDisplay()); foundCount++; } } @@ -1244,8 +1255,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return true; } - protected IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInPreExpandedValueSet( - ValidationOptions theValidationOptions, + protected IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedValueSet( + ConceptValidationOptions theValidationOptions, ValueSet theValueSet, String theSystem, String theCode, String theDisplay, Coding theCoding, CodeableConcept theCodeableConcept) { ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet.hasId(), "ValueSet.id is required"); @@ -1253,7 +1264,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { List concepts = new ArrayList<>(); if (isNotBlank(theCode)) { - if (theValidationOptions.isGuessSystem()) { + if (theValidationOptions.isInferSystem()) { concepts.addAll(myValueSetConceptDao.findByValueSetResourcePidAndCode(valueSetResourcePid.getIdAsLong(), theCode)); } else if (isNotBlank(theSystem)) { concepts.addAll(findByValueSetResourcePidSystemAndCode(valueSetResourcePid, theSystem, theCode)); @@ -1271,19 +1282,40 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } } + } else { + return null; } - for (TermValueSetConcept concept : concepts) { - if (isNotBlank(theDisplay) && theDisplay.equals(concept.getDisplay())) { - return new IFhirResourceDaoValueSet.ValidateCodeResult(true, "Validation succeeded", concept.getDisplay()); + if (theValidationOptions.isValidateDisplay() && concepts.size() > 0) { + for (TermValueSetConcept concept : concepts) { + if (isBlank(theDisplay) || isBlank(concept.getDisplay()) || theDisplay.equals(concept.getDisplay())) { + return new IValidationSupport.CodeValidationResult() + .setCode(concept.getCode()) + .setDisplay(concept.getDisplay()); + } } + + return createFailureCodeValidationResult(theSystem, theCode, " - Concept Display \"" + theDisplay + "\" does not match expected \"" + concepts.get(0).getDisplay() + "\"").setDisplay(concepts.get(0).getDisplay()); } if (!concepts.isEmpty()) { - return new IFhirResourceDaoValueSet.ValidateCodeResult(true, "Validation succeeded", concepts.get(0).getDisplay()); + return new IValidationSupport.CodeValidationResult() + .setCode(concepts.get(0).getCode()) + .setDisplay(concepts.get(0).getDisplay()); } - return null; + return createFailureCodeValidationResult(theSystem, theCode); + } + + private CodeValidationResult createFailureCodeValidationResult(String theSystem, String theCode) { + String append = ""; + return createFailureCodeValidationResult(theSystem, theCode, append); + } + + private CodeValidationResult createFailureCodeValidationResult(String theSystem, String theCode, String theAppend) { + return new CodeValidationResult() + .setSeverity(IssueSeverity.ERROR) + .setMessage("Unknown code {" + theSystem + "}" + theCode + theAppend); } private List findByValueSetResourcePidSystemAndCode(ResourcePersistentId theResourcePid, String theSystem, String theCode) { @@ -1499,6 +1531,13 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return; } + if (source == null && theConceptMap.hasSourceCanonicalType()) { + source = theConceptMap.getSourceCanonicalType().getValueAsString(); + } + if (target == null && theConceptMap.hasTargetCanonicalType()) { + target = theConceptMap.getTargetCanonicalType().getValueAsString(); + } + /* * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions. */ @@ -1526,17 +1565,28 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { if (theConceptMap.hasGroup()) { TermConceptMapGroup termConceptMapGroup; for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) { - if (isBlank(group.getSource())) { + + String groupSource = group.getSource(); + if (isBlank(groupSource)) { + groupSource = source; + } + if (isBlank(groupSource)) { throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.source"); } - if (isBlank(group.getTarget())) { + + String groupTarget = group.getTarget(); + if (isBlank(groupTarget)) { + groupTarget = target; + } + if (isBlank(groupTarget)) { throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.target"); } + termConceptMapGroup = new TermConceptMapGroup(); termConceptMapGroup.setConceptMap(termConceptMap); - termConceptMapGroup.setSource(group.getSource()); + termConceptMapGroup.setSource(groupSource); termConceptMapGroup.setSourceVersion(group.getSourceVersion()); - termConceptMapGroup.setTarget(group.getTarget()); + termConceptMapGroup.setTarget(groupTarget); termConceptMapGroup.setTargetVersion(group.getTargetVersion()); myConceptMapGroupDao.save(termConceptMapGroup); @@ -1636,6 +1686,58 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } + @Override + public CodeValidationResult validateCode(ConceptValidationOptions theOptions, IIdType theValueSetId, String theValueSetUrl, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { + + CodeableConcept codeableConcept = toCanonicalCodeableConcept(theCodeableConcept); + boolean haveCodeableConcept = codeableConcept != null && codeableConcept.getCoding().size() > 0; + + Coding coding = toCanonicalCoding(theCoding); + boolean haveCoding = coding != null && coding.isEmpty() == false; + + boolean haveCode = theCode != null && theCode.isEmpty() == false; + + if (!haveCodeableConcept && !haveCoding && !haveCode) { + throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate"); + } + if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { + throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)"); + } + + boolean haveIdentifierParam = isNotBlank(theValueSetUrl); + String valueSetUrl; + if (theValueSetId != null) { + IBaseResource valueSet = myDaoRegistry.getResourceDao("ValueSet").read(theValueSetId); + valueSetUrl = CommonCodeSystemsTerminologyService.getValueSetUrl(valueSet); + } else if (haveIdentifierParam) { + valueSetUrl = theValueSetUrl; + } else { + throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate."); + } + + ValidationSupportContext validationContext = new ValidationSupportContext(provideValidationSupport()); + + String code = theCode; + String system = theSystem; + String display = theDisplay; + + if (haveCodeableConcept) { + for (int i = 0; i < codeableConcept.getCoding().size(); i++) { + Coding nextCoding = codeableConcept.getCoding().get(i); + CodeValidationResult nextValidation = validateCode(validationContext, theOptions, nextCoding.getSystem(), nextCoding.getCode(), nextCoding.getDisplay(), valueSetUrl); + if (nextValidation.isOk() || i == codeableConcept.getCoding().size() - 1) { + return nextValidation; + } + } + } else if (haveCoding) { + system = coding.getSystem(); + code = coding.getCode(); + display = coding.getDisplay(); + } + + return validateCode(validationContext, theOptions, system, code, display, valueSetUrl); + } + private boolean isNotSafeToPreExpandValueSets() { return myDeferredStorageSvc != null && !myDeferredStorageSvc.isStorageQueueEmpty(); } @@ -1997,7 +2099,36 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return null; } - Optional validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theValueSetUrl, String theCodeSystem, String theCode) { + + @CoverageIgnore + @Override + public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { + invokeRunnableForUnitTest(); + + if (isNotBlank(theValueSetUrl)) { + return validateCodeInValueSet(theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystem, theCode, theDisplay); + } + + TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + Optional codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c -> new VersionIndependentConcept(theCodeSystem, c.getCode()))); + + if (codeOpt != null && codeOpt.isPresent()) { + VersionIndependentConcept code = codeOpt.get(); + if (!theOptions.isValidateDisplay() || (isNotBlank(code.getDisplay()) && isNotBlank(theDisplay) && code.getDisplay().equals(theDisplay))) { + return new CodeValidationResult() + .setCode(code.getCode()) + .setDisplay(code.getDisplay()); + } else { + return createFailureCodeValidationResult(theCodeSystem, theCode, " - Concept Display \"" + code.getDisplay() + "\" does not match expected \"" + code.getDisplay() + "\"").setDisplay(code.getDisplay()); + } + } + + return createFailureCodeValidationResult(theCodeSystem, theCode); + } + + + IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theValueSetUrl, String theCodeSystem, String theCode, String theDisplay) { IBaseResource valueSet = theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl); // If we don't have a PID, this came from some source other than the JPA @@ -2006,24 +2137,26 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { Long pid = IDao.RESOURCE_PID.get((IAnyResource) valueSet); if (pid != null) { if (isValueSetPreExpandedForCodeValidation(valueSet)) { - IFhirResourceDaoValueSet.ValidateCodeResult outcome = validateCodeIsInPreExpandedValueSet(new ValidationOptions(), valueSet, theCodeSystem, theCode, null, null, null); - if (outcome != null && outcome.isResult()) { - return Optional.of(new VersionIndependentConcept(theCodeSystem, theCode)); - } + return validateCodeIsInPreExpandedValueSet(theValidationOptions, valueSet, theCodeSystem, theCode, null, null, null); } } } - ValueSet canonicalValueSet = toCanonicalValueSet(valueSet); - VersionIndependentConcept wantConcept = new VersionIndependentConcept(theCodeSystem, theCode); - ValueSetExpansionOptions expansionOptions = new ValueSetExpansionOptions() - .setFailOnMissingCodeSystem(false); + CodeValidationResult retVal = null; + if (valueSet != null) { + retVal = new InMemoryTerminologyServerValidationSupport(myContext).validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, valueSet); + } else { + String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]"; + retVal = createFailureCodeValidationResult(theCodeSystem, theCode, append); + } + + if (retVal == null) { + String append = " - Unable to expand ValueSet[" + theValueSetUrl + "]"; + retVal = createFailureCodeValidationResult(theCodeSystem, theCode, append); + } + + return retVal; - List expansionOutcome = expandValueSetAndReturnVersionIndependentConcepts(expansionOptions, canonicalValueSet, wantConcept); - return expansionOutcome - .stream() - .filter(t -> (theValidationOptions.isInferSystem() || t.getSystem().equals(theCodeSystem)) && t.getCode().equals(theCode)) - .findFirst(); } @Override @@ -2148,6 +2281,12 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return false; } + @Nullable + protected abstract Coding toCanonicalCoding(@Nullable IBaseDatatype theCoding); + + @Nullable + protected abstract CodeableConcept toCanonicalCodeableConcept(@Nullable IBaseDatatype theCodeableConcept); + public static class Job implements HapiJob { @Autowired private ITermReadSvc myTerminologySvc; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu2.java index 541dd070b36..7f15699e201 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu2.java @@ -20,18 +20,23 @@ package ca.uhn.fhir.jpa.term; * #L% */ +import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; +import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.util.VersionIndependentConcept; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.springframework.beans.factory.annotation.Autowired; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -132,8 +137,34 @@ public class TermReadSvcDstu2 extends BaseTermReadSvcImpl { return retVal; } + @Nullable @Override - public IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInPreExpandedValueSet(ValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { + protected Coding toCanonicalCoding(@Nullable IBaseDatatype theCoding) { + Coding retVal = null; + if (theCoding != null) { + CodingDt coding = (CodingDt) theCoding; + retVal = new Coding(coding.getSystem(), coding.getCode(), coding.getDisplay()); + } + return retVal; + } + + @Nullable + @Override + protected CodeableConcept toCanonicalCodeableConcept(@Nullable IBaseDatatype theCodeableConcept) { + CodeableConcept outcome = null; + if (theCodeableConcept != null) { + outcome = new CodeableConcept(); + CodeableConceptDt cc = (CodeableConceptDt) theCodeableConcept; + outcome.setText(cc.getText()); + for (CodingDt next : cc.getCoding()) { + outcome.addCoding(toCanonicalCoding(next)); + } + } + return outcome; + } + + @Override + public CodeValidationResult validateCodeIsInPreExpandedValueSet(ConceptValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { throw new UnsupportedOperationException(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java index dadcecf2dd1..6b266ae52f3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcDstu3.java @@ -12,6 +12,8 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.ValidateUtil; import ca.uhn.fhir.util.VersionIndependentConcept; +import org.hl7.fhir.convertors.VersionConvertor_30_40; +import org.hl7.fhir.convertors.VersionConvertor_40_50; import org.hl7.fhir.convertors.conv30_40.CodeSystem30_40; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeableConcept; @@ -26,6 +28,7 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.Nullable; import java.util.Optional; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -103,6 +106,20 @@ public class TermReadSvcDstu3 extends BaseTermReadSvcImpl implements IValidation return CodeSystem30_40.convertCodeSystem((CodeSystem)theCodeSystem); } + @Override + @Nullable + protected org.hl7.fhir.r4.model.Coding toCanonicalCoding(IBaseDatatype theCoding) { + return VersionConvertor_30_40.convertCoding((org.hl7.fhir.dstu3.model.Coding) theCoding); + } + + @Override + @Nullable + protected org.hl7.fhir.r4.model.CodeableConcept toCanonicalCodeableConcept(IBaseDatatype theCoding) { + return VersionConvertor_30_40.convertCodeableConcept((org.hl7.fhir.dstu3.model.CodeableConcept) theCoding); + } + + + @Override public void expandValueSet(ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand, IValueSetConceptAccumulator theValueSetCodeAccumulator) { ValueSet valueSetToExpand = (ValueSet) theValueSetToExpand; @@ -130,35 +147,6 @@ public class TermReadSvcDstu3 extends BaseTermReadSvcImpl implements IValidation return valueSetR4; } - @CoverageIgnore - @Override - public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { - Optional codeOpt = Optional.empty(); - boolean haveValidated = false; - - if (isNotBlank(theValueSetUrl)) { - codeOpt = super.validateCodeInValueSet(theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystem, theCode); - haveValidated = true; - } - - if (!haveValidated) { - TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c -> new VersionIndependentConcept(theCodeSystem, c.getCode()))); - } - - if (codeOpt != null && codeOpt.isPresent()) { - VersionIndependentConcept code = codeOpt.get(); - IValidationSupport.CodeValidationResult retVal = new IValidationSupport.CodeValidationResult() - .setCode(code.getCode()); - return retVal; - } - - return new IValidationSupport.CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Unknown code {" + theCodeSystem + "}" + theCode); - } - @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { return super.lookupCode(theSystem, theCode); @@ -170,7 +158,7 @@ public class TermReadSvcDstu3 extends BaseTermReadSvcImpl implements IValidation } @Override - public IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInPreExpandedValueSet(ValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { + public IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedValueSet(ConceptValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null"); ValueSet valueSet = (ValueSet) theValueSet; org.hl7.fhir.r4.model.ValueSet valueSetR4 = convertValueSet(valueSet); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java index d4e3bf2362e..2075725dc2f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR4.java @@ -5,28 +5,19 @@ import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4; import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException; -import ca.uhn.fhir.util.CoverageIgnore; -import ca.uhn.fhir.util.VersionIndependentConcept; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.ValueSet; -import org.hl7.fhir.utilities.validation.ValidationOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.support.TransactionTemplate; import javax.transaction.Transactional; -import java.util.Optional; - -import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * #%L @@ -98,50 +89,29 @@ public class TermReadSvcR4 extends BaseTermReadSvcImpl implements ITermReadSvcR4 return (CodeSystem) theCodeSystem; } - @CoverageIgnore - @Override - public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { - invokeRunnableForUnitTest(); - - Optional codeOpt = Optional.empty(); - boolean haveValidated = false; - - if (isNotBlank(theValueSetUrl)) { - codeOpt = super.validateCodeInValueSet(theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystem, theCode); - haveValidated = true; - } - - if (!haveValidated) { - TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c -> new VersionIndependentConcept(theCodeSystem, c.getCode()))); - } - - if (codeOpt != null && codeOpt.isPresent()) { - VersionIndependentConcept code = codeOpt.get(); - IValidationSupport.CodeValidationResult retVal = new IValidationSupport.CodeValidationResult() - .setCode(code.getCode()); - return retVal; - } - - return new IValidationSupport.CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Unknown code {" + theCodeSystem + "}" + theCode); - } - @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { return super.lookupCode(theSystem, theCode); } @Override - public IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInPreExpandedValueSet(ValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { + public IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedValueSet(ConceptValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { ValueSet valueSet = (ValueSet) theValueSet; - Coding coding = (Coding) theCoding; - CodeableConcept codeableConcept = (CodeableConcept) theCodeableConcept; + Coding coding = toCanonicalCoding(theCoding); + CodeableConcept codeableConcept = toCanonicalCodeableConcept(theCodeableConcept); return super.validateCodeIsInPreExpandedValueSet(theOptions, valueSet, theSystem, theCode, theDisplay, coding, codeableConcept); } + @Override + protected Coding toCanonicalCoding(IBaseDatatype theCoding) { + return (Coding) theCoding; + } + + @Override + protected CodeableConcept toCanonicalCodeableConcept(IBaseDatatype theCodeableConcept) { + return (CodeableConcept) theCodeableConcept; + } + @Override public boolean isValueSetPreExpandedForCodeValidation(IBaseResource theValueSet) { ValueSet valueSet = (ValueSet) theValueSet; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR5.java index a43b825186c..2fbda8d9d88 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcR5.java @@ -6,17 +6,15 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.api.ITermReadSvcR5; import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException; import ca.uhn.fhir.util.ValidateUtil; -import ca.uhn.fhir.util.VersionIndependentConcept; +import org.hl7.fhir.convertors.VersionConvertor_40_50; import org.hl7.fhir.convertors.conv40_50.CodeSystem40_50; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.CodeSystem; -import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.ValueSet; @@ -24,13 +22,9 @@ import org.hl7.fhir.utilities.TerminologyServiceOptions; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.Nullable; import javax.transaction.Transactional; -import java.util.Optional; - -import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * #%L @@ -86,57 +80,18 @@ public class TermReadSvcR5 extends BaseTermReadSvcImpl implements IValidationSup return org.hl7.fhir.convertors.conv40_50.ValueSet40_50.convertValueSet(valueSetR5); } - @Override - public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { - Optional codeOpt = Optional.empty(); - boolean haveValidated = false; - - if (isNotBlank(theValueSetUrl)) { - codeOpt = super.validateCodeInValueSet(theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystem, theCode); - haveValidated = true; - } - - if (!haveValidated) { - TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - codeOpt = txTemplate.execute(t -> findCode(theCodeSystem, theCode).map(c -> new VersionIndependentConcept(theCodeSystem, c.getCode()))); - } - - if (codeOpt != null && codeOpt.isPresent()) { - VersionIndependentConcept code = codeOpt.get(); - ConceptDefinitionComponent def = new ConceptDefinitionComponent(); - def.setCode(code.getCode()); - IValidationSupport.CodeValidationResult retVal = new IValidationSupport.CodeValidationResult() - .setCode(code.getCode()); - return retVal; - } - - return new IValidationSupport.CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setCode("Unknown code {" + theCodeSystem + "}" + theCode); - } - - @Override - public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { - return super.lookupCode(theSystem, theCode); - } - @Override public FhirContext getFhirContext() { return myContext; } @Override - public IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInPreExpandedValueSet(ValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { + public CodeValidationResult validateCodeIsInPreExpandedValueSet(ConceptValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept) { ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null"); ValueSet valueSet = (ValueSet) theValueSet; org.hl7.fhir.r4.model.ValueSet valueSetR4 = toCanonicalValueSet(valueSet); - Coding coding = (Coding) theCoding; - org.hl7.fhir.r4.model.Coding codingR4 = null; - if (coding != null) { - codingR4 = new org.hl7.fhir.r4.model.Coding(coding.getSystem(), coding.getCode(), coding.getDisplay()); - } + org.hl7.fhir.r4.model.Coding codingR4 = toCanonicalCoding(theCoding); CodeableConcept codeableConcept = (CodeableConcept) theCodeableConcept; org.hl7.fhir.r4.model.CodeableConcept codeableConceptR4 = null; @@ -147,9 +102,22 @@ public class TermReadSvcR5 extends BaseTermReadSvcImpl implements IValidationSup } } - return super.validateCodeIsInPreExpandedValueSet(new TerminologyServiceOptions(), valueSetR4, theSystem, theCode, theDisplay, codingR4, codeableConceptR4); + return super.validateCodeIsInPreExpandedValueSet(theOptions, valueSetR4, theSystem, theCode, theDisplay, codingR4, codeableConceptR4); } + @Override + @Nullable + protected org.hl7.fhir.r4.model.Coding toCanonicalCoding(IBaseDatatype theCoding) { + return VersionConvertor_40_50.convertCoding((Coding) theCoding); + } + + @Override + @Nullable + protected org.hl7.fhir.r4.model.CodeableConcept toCanonicalCodeableConcept(IBaseDatatype theCoding) { + return VersionConvertor_40_50.convertCodeableConcept((CodeableConcept) theCoding); + } + + @Override protected org.hl7.fhir.r4.model.ValueSet toCanonicalValueSet(IBaseResource theValueSet) throws org.hl7.fhir.exceptions.FHIRException { return org.hl7.fhir.convertors.conv40_50.ValueSet40_50.convertValueSet((ValueSet) theValueSet); 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 9411117e80b..bfac6ae33d9 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 @@ -1,19 +1,22 @@ package ca.uhn.fhir.jpa.term.api; +import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.model.TranslationRequest; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.IValueSetConceptAccumulator; +import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; +import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.util.VersionIndependentConcept; import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.ConceptMap; @@ -105,7 +108,12 @@ public interface ITermReadSvc extends IValidationSupport { /** * Version independent */ - IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInPreExpandedValueSet(ValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept); + CodeValidationResult validateCode(ConceptValidationOptions theOptions, IIdType theValueSetId, String theValueSetUrl, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept); + + /** + * Version independent + */ + CodeValidationResult validateCodeIsInPreExpandedValueSet(ConceptValidationOptions theOptions, IBaseResource theValueSet, String theSystem, String theCode, String theDisplay, IBaseDatatype theCoding, IBaseDatatype theCodeableConcept); boolean isValueSetPreExpandedForCodeValidation(ValueSet theValueSet); @@ -114,5 +122,4 @@ public interface ITermReadSvc extends IValidationSupport { */ boolean isValueSetPreExpandedForCodeValidation(IBaseResource theValueSet); - } 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 c285020d284..2bf9e206c9a 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 @@ -66,13 +66,13 @@ public class JpaValidationSupportChain extends ValidationSupportChain { @PostConstruct public void postConstruct() { - addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext)); addValidationSupport(myDefaultProfileValidationSupport); addValidationSupport(myJpaValidationSupport); addValidationSupport(myTerminologyService); addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext)); addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext)); addValidationSupport(myNpmJpaValidationSupport); + addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext)); } } 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 659d68dbb9c..b2703eedb01 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 @@ -7,6 +7,7 @@ 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.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.dbcp2.BasicDataSource; import org.hibernate.dialect.H2Dialect; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index 1dfb5d78f78..060069e9f09 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -56,6 +56,7 @@ public class TestR4Config extends BaseJavaConfigR4 { private Exception myLastStackTrace; + @Override @Bean public IBatchJobSubmitter batchJobSubmitter() { return new BatchJobSubmitterImpl(); @@ -118,7 +119,7 @@ public class TestR4Config extends BaseJavaConfigR4 { retVal.setDriver(new org.h2.Driver()); retVal.setUrl("jdbc:h2:mem:testdb_r4"); - retVal.setMaxWaitMillis(10000); + retVal.setMaxWaitMillis(30000); retVal.setUsername(""); retVal.setPassword(""); retVal.setMaxTotal(ourMaxThreads); @@ -126,7 +127,8 @@ public class TestR4Config extends BaseJavaConfigR4 { SLF4JLogLevel level = SLF4JLogLevel.INFO; DataSource dataSource = ProxyDataSourceBuilder .create(retVal) - .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) +// .logQueryBySlf4j(level) + .logSlowQueryBySlf4j(10, TimeUnit.SECONDS, level) .beforeQuery(new BlockLargeNumbersOfParamsListener()) .afterQuery(captureQueriesListener()) .afterQuery(new CurrentThreadCaptureQueriesListener()) 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 dae3bdc868c..b08b4c9ebc6 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 @@ -9,8 +9,8 @@ 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.config.BaseConfig; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.model.util.JpaConstants; @@ -22,6 +22,8 @@ import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionLoader; +import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.model.dstu2.resource.Bundle; @@ -29,6 +31,7 @@ import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.test.BaseTest; import ca.uhn.fhir.test.utilities.LoggingExtension; @@ -59,7 +62,6 @@ 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; @@ -128,6 +130,10 @@ public abstract class BaseJpaTest extends BaseTest { @Autowired protected IPartitionLookupSvc myPartitionConfigSvc; @Autowired + protected SubscriptionRegistry mySubscriptionRegistry; + @Autowired + protected SubscriptionLoader mySubscriptionLoader; + @Autowired private IdHelperService myIdHelperService; @Autowired private MemoryCacheService myMemoryCacheService; @@ -419,6 +425,23 @@ public abstract class BaseJpaTest extends BaseTest { return retVal.toArray(new String[0]); } + protected void waitForActivatedSubscriptionCount(int theSize) throws Exception { + for (int i = 0; ; i++) { + if (i == 10) { + fail("Failed to init subscriptions"); + } + try { + mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); + break; + } catch (ResourceVersionConflictException e) { + Thread.sleep(250); + } + } + + TestUtil.waitForSize(theSize, () -> mySubscriptionRegistry.size()); + Thread.sleep(500); + } + @BeforeAll public static void beforeClassRandomizeLocale() { randomizeLocale(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java index a741732d435..488559b1c3b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java @@ -590,19 +590,27 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { @Test public void testDeleteFailsIfIncomingLinks() { String methodName = "testDeleteFailsIfIncomingLinks"; + SearchParameterMap map; + List found; + Organization org = new Organization(); org.setName(methodName); IIdType orgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); + map = SearchParameterMap.newSynchronous(); + map.add("_id", new StringParam(orgId.getIdPart())); + map.addRevInclude(new Include("*")); + found = toUnqualifiedVersionlessIds(myOrganizationDao.search(map)); + assertThat(found, contains(orgId)); + Patient patient = new Patient(); patient.addName().addFamily(methodName); patient.getManagingOrganization().setReference(orgId); IIdType patId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); - SearchParameterMap map = new SearchParameterMap(); map.add("_id", new StringParam(orgId.getIdPart())); map.addRevInclude(new Include("*")); - List found = toUnqualifiedVersionlessIds(myOrganizationDao.search(map)); + found = toUnqualifiedVersionlessIds(myOrganizationDao.search(map)); assertThat(found, contains(orgId, patId)); try { @@ -613,9 +621,21 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { } + map = SearchParameterMap.newSynchronous(); + map.add("_id", new StringParam(orgId.getIdPart())); + map.addRevInclude(new Include("*")); + ourLog.info("***** About to perform search"); + found = toUnqualifiedVersionlessIds(myOrganizationDao.search(map)); + + runInTransaction(()->{ + ourLog.info("Resources:\n * {}", myResourceTableDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); + }); + + assertThat(found.toString(), found, contains(orgId, patId)); + myPatientDao.delete(patId, mySrd); - map = new SearchParameterMap(); + map = SearchParameterMap.newSynchronous(); map.add("_id", new StringParam(orgId.getIdPart())); map.addRevInclude(new Include("*")); found = toUnqualifiedVersionlessIds(myOrganizationDao.search(map)); @@ -623,7 +643,7 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { myOrganizationDao.delete(orgId, mySrd); - map = new SearchParameterMap(); + map = SearchParameterMap.newSynchronous(); map.add("_id", new StringParam(orgId.getIdPart())); map.addRevInclude(new Include("*")); found = toUnqualifiedVersionlessIds(myOrganizationDao.search(map)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java index fd6f6be67e0..4878070ca41 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoValueSetDstu2Test.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.dao.dstu2; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.resource.ValueSet; @@ -8,9 +8,7 @@ import ca.uhn.fhir.model.primitive.CodeDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.UriDt; -import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.transaction.annotation.Transactional; @@ -40,30 +38,42 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test { } @Test - public void testValidateCodeOperationByCodeAndSystemBad() { - UriDt valueSetIdentifier = null; + public void testValidateCodeOperationByCodeAndSystemBadCode() { + UriDt valueSetIdentifier = new UriDt("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2"); IdDt id = null; CodeDt code = new CodeDt("8450-9-XXX"); UriDt system = new UriDt("http://loinc.org"); StringDt display = null; CodingDt coding = null; CodeableConceptDt codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertFalse(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertFalse(result.isOk()); } @Test - public void testValidateCodeOperationByCodeAndSystemGood() { - UriDt valueSetIdentifier = null; + public void testValidateCodeOperationByCodeAndSystemBadSystem() { + UriDt valueSetIdentifier = new UriDt("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2"); + IdDt id = null; + CodeDt code = new CodeDt("8450-9-XXX"); + UriDt system = new UriDt("http://zzz"); + StringDt display = null; + CodingDt coding = null; + CodeableConceptDt codeableConcept = null; + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertFalse(result.isOk()); + } + + @Test + public void testValidateCodeOperationByIdentifierCodeInCsButNotInVs() { + UriDt valueSetIdentifier = new UriDt("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2"); IdDt id = null; CodeDt code = new CodeDt("8450-9"); UriDt system = new UriDt("http://loinc.org"); StringDt display = null; CodingDt coding = null; CodeableConceptDt codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); - assertEquals("Systolic blood pressure--expiration", result.getDisplay()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertFalse(result.isOk()); } @Test @@ -75,8 +85,8 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test { StringDt display = null; CodingDt coding = null; CodeableConceptDt codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -89,9 +99,10 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test { StringDt display = new StringDt("Systolic blood pressure at First encounterXXXX"); CodingDt coding = null; CodeableConceptDt codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertFalse(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertFalse(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); + assertEquals("Concept Display \"Systolic blood pressure at First encounterXXXX\" does not match expected \"Systolic blood pressure at First encounter\"", result.getMessage()); } @Test @@ -103,8 +114,8 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test { StringDt display = new StringDt("Systolic blood pressure at First encounter"); CodingDt coding = null; CodeableConceptDt codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -117,8 +128,8 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test { StringDt display = null; CodingDt coding = null; CodeableConceptDt codeableConcept = new CodeableConceptDt("http://loinc.org", "11378-7"); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -131,8 +142,8 @@ public class FhirResourceDaoValueSetDstu2Test extends BaseJpaDstu2Test { StringDt display = null; CodingDt coding = null; CodeableConceptDt codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } 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 9a38ac1a693..720a4bd0a3d 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 @@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao; +import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao; import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.model.entity.ModelConfig; @@ -340,6 +341,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { private IValidationSupport myJpaValidationSupportChainDstu3; @Autowired private IBulkDataExportSvc myBulkDataExportSvc; + @Autowired + protected ITermValueSetDao myTermValueSetDao; @AfterEach() public void afterCleanupDao() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java index cf667709acb..eeacca3ea22 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValueSetTest.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; +import ca.uhn.fhir.jpa.entity.TermValueSet; +import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.CodeableConcept; @@ -14,7 +14,6 @@ import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -52,6 +51,51 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { } + @Test + public void testExpandValueSetWithIso3166() throws IOException { + ValueSet vs = loadResourceFromClasspath(ValueSet.class, "/dstu3/nl/LandISOCodelijst-2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000.json"); + myValueSetDao.create(vs); + + runInTransaction(() -> { + TermValueSet vsEntity = myTermValueSetDao.findByUrl("http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000").orElseThrow(() -> new IllegalStateException()); + assertEquals(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED, vsEntity.getExpansionStatus()); + }); + + IValidationSupport.CodeValidationResult validationOutcome; + UriType vsIdentifier = new UriType("http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000"); + CodeType code = new CodeType(); + CodeType system = new CodeType("urn:iso:std:iso:3166"); + + // Validate good + code.setValue("NL"); + validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd); + assertEquals(true, validationOutcome.isOk()); + + // Validate bad + code.setValue("QQ"); + validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd); + assertEquals(false, validationOutcome.isOk()); + + myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); + + runInTransaction(() -> { + TermValueSet vsEntity = myTermValueSetDao.findByUrl("http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000").orElseThrow(() -> new IllegalStateException()); + assertEquals(TermValueSetPreExpansionStatusEnum.EXPANDED, vsEntity.getExpansionStatus()); + }); + + // Validate good + code.setValue("NL"); + validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd); + assertEquals(true, validationOutcome.isOk()); + + // Validate bad + code.setValue("QQ"); + validationOutcome = myValueSetDao.validateCode(vsIdentifier, null, code, system, null, null, null, mySrd); + assertEquals(false, validationOutcome.isOk()); + + } + + @Test @Disabled public void testBuiltInValueSetFetchAndExpand() { @@ -144,33 +188,6 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { assertThat(resp, not(containsString(""))); } - @Test - public void testValidateCodeOperationByCodeAndSystemBad() { - UriType valueSetIdentifier = null; - IdType id = null; - CodeType code = new CodeType("8450-9-XXX"); - UriType system = new UriType("http://acme.org"); - StringType display = null; - Coding coding = null; - CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertFalse(result.isResult()); - } - - @Test - public void testValidateCodeOperationByCodeAndSystemGood() { - UriType valueSetIdentifier = null; - IdType id = null; - CodeType code = new CodeType("8450-9"); - UriType system = new UriType("http://acme.org"); - StringType display = null; - Coding coding = null; - CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); - assertEquals("Systolic blood pressure--expiration", result.getDisplay()); - } - @Test public void testValidateCodeOperationByIdentifierAndCodeAndSystem() { UriType valueSetIdentifier = new UriType("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2"); @@ -180,8 +197,8 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { StringType display = null; Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -194,8 +211,8 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { StringType display = new StringType("Systolic blood pressure at First encounterXXXX"); Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertFalse(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertFalse(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -208,8 +225,8 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { StringType display = new StringType("Systolic blood pressure at First encounter"); Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -222,8 +239,8 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { StringType display = null; Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -237,8 +254,8 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { Coding coding = null; CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.addCoding().setSystem("http://acme.org").setCode("11378-7"); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -250,13 +267,12 @@ public class FhirResourceDaoDstu3ValueSetTest extends BaseJpaDstu3Test { StringType vsIdentifier = new StringType("http://hl7.org/fhir/ValueSet/administrative-gender"); StringType code = new StringType("male"); StringType system = new StringType("http://hl7.org/fhir/administrative-gender"); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(vsIdentifier, null, code, system, display, coding, codeableConcept, mySrd); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(vsIdentifier, null, code, system, display, coding, codeableConcept, mySrd); ourLog.info(result.getMessage()); - assertTrue(result.isResult(), result.getMessage()); + assertTrue(result.isOk(), result.getMessage()); } - } 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 0630319a005..6f82419b86c 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 @@ -447,8 +447,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao; @Autowired protected ICacheWarmingSvc myCacheWarmingSvc; - @Autowired - protected SubscriptionRegistry mySubscriptionRegistry; protected IServerInterceptor myInterceptor; @Autowired protected DaoRegistry myDaoRegistry; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java index 972c3adc0f9..53d167270b0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java @@ -163,7 +163,7 @@ public class BaseR4SearchLastN extends BaseJpaTest { } private Date calculateObservationDateFromOffset(Integer theTimeOffset, Integer theObservationIndex) { - int milliSecondsPerHour = 3600 * 1000; + long milliSecondsPerHour = 3600L * 1000L; // Generate a Date by subtracting a calculated number of hours from the static observationDate property. return new Date(observationDate.getTimeInMillis() - (milliSecondsPerHour * (theTimeOffset + theObservationIndex))); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java index 23a59c9e29b..f03d74debe4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.api.model.TranslationRequest; import ca.uhn.fhir.jpa.api.model.TranslationResult; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.ConceptMap; @@ -1042,6 +1043,49 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { }); } + /** + * Some US core ConceptMaps use this style, e.g: + * + * http://hl7.org/fhir/us/core/ConceptMap/ndc-cvx + */ + @Test + public void testUploadConceptMapWithOnlyCanonicalSourceAtConceptMapLevel() { + + ConceptMap cm = new ConceptMap(); + cm.setUrl("http://foo"); + cm.setSource(new CanonicalType("http://source")); + cm.setTarget(new CanonicalType("http://target")); + cm.addGroup().addElement().setCode("source1").addTarget().setCode("target1").setEquivalence(ConceptMapEquivalence.EQUAL); + myConceptMapDao.create(cm); + + runInTransaction(()->{ + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem("http://source") + .setCode("source1"); + translationRequest.setTarget(new UriType("http://target")); + + ourLog.info("*** About to translate"); + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + ourLog.info("*** Done translating"); + + assertTrue(translationResult.getResult().booleanValue()); + assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); + + assertEquals(1, translationResult.getMatches().size()); + + TranslationMatch translationMatch = translationResult.getMatches().get(0); + assertEquals("equal", translationMatch.getEquivalence().getCode()); + Coding concept = translationMatch.getConcept(); + assertEquals("target1", concept.getCode()); + assertEquals(null, concept.getDisplay()); + assertEquals("http://target", concept.getSystem()); + }); + + + } + + @Test public void testUploadAndApplyR4DemoConceptMap() throws IOException { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConcurrentWriteTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConcurrentWriteTest.java new file mode 100644 index 00000000000..704a969e32f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConcurrentWriteTest.java @@ -0,0 +1,442 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.interceptor.executor.InterceptorService; +import ca.uhn.fhir.jpa.interceptor.UserRequestRetryVersionConflictsInterceptor; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.HapiExtensions; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings({"unchecked", "deprecation", "Duplicates"}) +public class FhirResourceDaoR4ConcurrentWriteTest extends BaseJpaR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ConcurrentWriteTest.class); + private ExecutorService myExecutor; + private UserRequestRetryVersionConflictsInterceptor myRetryInterceptor; + + + @BeforeEach + public void before() { + myExecutor = Executors.newFixedThreadPool(10); + myRetryInterceptor = new UserRequestRetryVersionConflictsInterceptor(); + + RestfulServer server = new RestfulServer(myFhirCtx); + when(mySrd.getServer()).thenReturn(server); + + } + + @AfterEach + public void after() { + myExecutor.shutdown(); + myInterceptorRegistry.unregisterInterceptor(myRetryInterceptor); + } + + @Test + public void testCreateWithClientAssignedId() { + myInterceptorRegistry.registerInterceptor(myRetryInterceptor); + String value = UserRequestRetryVersionConflictsInterceptor.RETRY + "; " + UserRequestRetryVersionConflictsInterceptor.MAX_RETRIES + "=10"; + when(mySrd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.singletonList(value)); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Patient p = new Patient(); + p.setId("ABC"); + p.setActive(true); + p.addIdentifier().setValue("VAL" + i); + Runnable task = () -> myPatientDao.update(p, mySrd); + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (Exception e) { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + + // Make sure we saved the object + Patient patient = myPatientDao.read(new IdType("Patient/ABC")); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + assertEquals(true, patient.getActive()); + + } + + @Test + public void testCreateWithUniqueConstraint() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-gender"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setCode("gender"); + sp.setExpression("Patient.gender"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-gender-unique"); + sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.addBase("Patient"); + sp.addComponent() + .setExpression("Patient") + .setDefinition("SearchParameter/patient-gender"); + sp.addExtension() + .setUrl(HapiExtensions.EXT_SP_UNIQUE) + .setValue(new BooleanType(true)); + mySearchParameterDao.update(sp); + + mySearchParamRegistry.forceRefresh(); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.MALE); + p.addIdentifier().setValue("VAL" + i); + Runnable task = () -> { + try { + myPatientDao.create(p); + } catch (PreconditionFailedException e) { + // expected - This is as a result of the unique SP + assertThat(e.getMessage(), containsString("duplicate index matching query: Patient?gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale")); + } catch (ResourceVersionConflictException e) { + // expected - This is as a result of the unique SP + assertThat(e.getMessage(), containsString("would have resulted in a duplicate value for a unique index")); + } + }; + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (Exception e) { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + + runInTransaction(() -> { + ourLog.info("Uniques:\n * " + myResourceIndexedCompositeStringUniqueDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + }); + + // Make sure we saved the object + myCaptureQueriesListener.clear(); + IBundleProvider search = myPatientDao.search(SearchParameterMap.newSynchronous("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"))); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(1, search.sizeOrThrowNpe()); + + } + + @Test + public void testDelete() { + myInterceptorRegistry.registerInterceptor(myRetryInterceptor); + String value = UserRequestRetryVersionConflictsInterceptor.RETRY + "; " + UserRequestRetryVersionConflictsInterceptor.MAX_RETRIES + "=100"; + when(mySrd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.singletonList(value)); + + IIdType patientId = runInTransaction(() -> { + Patient p = new Patient(); + p.setActive(true); + return myPatientDao.create(p).getId().toUnqualifiedVersionless(); + }); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + // Submit an update + Patient p = new Patient(); + p.setId(patientId); + p.addIdentifier().setValue("VAL" + i); + Runnable task = () -> myPatientDao.update(p, mySrd); + Future future = myExecutor.submit(task); + futures.add(future); + + // Submit a delete + task = () -> myPatientDao.delete(patientId, mySrd); + future = myExecutor.submit(task); + futures.add(future); + + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (Exception e) { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + + // Make sure we saved the object + IBundleProvider patient = myPatientDao.history(patientId, null, null, null); + assertThat(patient.sizeOrThrowNpe(), greaterThanOrEqualTo(3)); + + } + + @Test + public void testNoRetryRequest() { + myInterceptorRegistry.registerInterceptor(myRetryInterceptor); + when(mySrd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.emptyList()); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Patient p = new Patient(); + p.setId("ABC"); + p.setActive(true); + p.addIdentifier().setValue("VAL" + i); + Runnable task = () -> myPatientDao.update(p, mySrd); + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (ExecutionException | InterruptedException e) { + if (e.getCause() instanceof ResourceVersionConflictException) { + // this is expected since we're not retrying + ourLog.info("Version conflict (expected): {}", e.getCause().toString()); + } else { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + } + + // Make sure we saved the object + Patient patient = myPatientDao.read(new IdType("Patient/ABC")); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + assertEquals(true, patient.getActive()); + + } + + @Test + public void testNoRetryInterceptor() { + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Patient p = new Patient(); + p.setId("ABC"); + p.setActive(true); + p.addIdentifier().setValue("VAL" + i); + Runnable task = () -> myPatientDao.update(p, mySrd); + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (ExecutionException | InterruptedException e) { + if (e.getCause() instanceof ResourceVersionConflictException) { + // this is expected since we're not retrying + ourLog.info("Version conflict (expected): {}", e.getCause().toString()); + } else { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + } + + // Make sure we saved the object + Patient patient = myPatientDao.read(new IdType("Patient/ABC")); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + assertEquals(true, patient.getActive()); + + } + + + @Test + public void testNoRequestDetails() { + myInterceptorRegistry.registerInterceptor(myRetryInterceptor); + when(mySrd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.emptyList()); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Patient p = new Patient(); + p.setId("ABC"); + p.setActive(true); + p.addIdentifier().setValue("VAL" + i); + Runnable task = () -> myPatientDao.update(p); + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (ExecutionException | InterruptedException e) { + if (e.getCause() instanceof ResourceVersionConflictException) { + // this is expected since we're not retrying + ourLog.info("Version conflict (expected): {}", e.getCause().toString()); + } else { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + } + + // Make sure we saved the object + Patient patient = myPatientDao.read(new IdType("Patient/ABC")); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + assertEquals(true, patient.getActive()); + + } + + + @Test + public void testPatch() { + myInterceptorRegistry.registerInterceptor(myRetryInterceptor); + String value = UserRequestRetryVersionConflictsInterceptor.RETRY + "; " + UserRequestRetryVersionConflictsInterceptor.MAX_RETRIES + "=10"; + when(mySrd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.singletonList(value)); + + Patient p = new Patient(); + p.addName().setFamily("FAMILY"); + IIdType pId = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + + Parameters patch = new Parameters(); + Parameters.ParametersParameterComponent operation = patch.addParameter(); + operation.setName("operation"); + operation + .addPart() + .setName("type") + .setValue(new CodeType("replace")); + operation + .addPart() + .setName("path") + .setValue(new StringType("Patient.name[0].family")); + operation + .addPart() + .setName("value") + .setValue(new StringType("FAMILY-" + i)); + + Runnable task = () -> myPatientDao.patch(pId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, patch, mySrd); + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (Exception e) { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + + // Make sure we saved the object + Patient patient = myPatientDao.read(pId); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + assertEquals("6", patient.getMeta().getVersionId()); + + } + + + + @Test + public void testTransactionWithCreate() { + myInterceptorRegistry.registerInterceptor(myRetryInterceptor); + + ServletRequestDetails srd = mock(ServletRequestDetails.class); + String value = UserRequestRetryVersionConflictsInterceptor.RETRY + "; " + UserRequestRetryVersionConflictsInterceptor.MAX_RETRIES + "=10"; + when(srd.getHeaders(eq(UserRequestRetryVersionConflictsInterceptor.HEADER_NAME))).thenReturn(Collections.singletonList(value)); + when(srd.getUserData()).thenReturn(new HashMap<>()); + when(srd.getServer()).thenReturn(new RestfulServer(myFhirCtx)); + when(srd.getInterceptorBroadcaster()).thenReturn(new InterceptorService()); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + + Patient p = new Patient(); + p.setId("ABC"); + p.setActive(true); + p.addIdentifier().setValue("VAL" + i); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle + .addEntry() + .setResource(p) + .getRequest() + .setMethod(Bundle.HTTPVerb.PUT) + .setUrl("Patient/ABC"); + Runnable task = () -> mySystemDao.transaction(srd, bundle); + + Future future = myExecutor.submit(task); + futures.add(future); + } + + // Look for failures + for (Future next : futures) { + try { + next.get(); + ourLog.info("Future produced success"); + } catch (Exception e) { + ourLog.info("Future produced exception: {}", e.toString()); + throw new AssertionError("Failed with message: " + e.toString(), e); + } + } + + // Make sure we saved the object + Patient patient = myPatientDao.read(new IdType("Patient/ABC")); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + assertEquals(true, patient.getActive()); + + } + +} 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 39cca68a358..6c426fee82c 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 @@ -441,7 +441,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { Observation obs = new Observation(); obs.getSubject().setReference("Patient/P"); - myObservationDao.update(obs); + myObservationDao.create(obs); SearchParameterMap map = new SearchParameterMap(); map.setLoadSynchronous(true); @@ -482,7 +482,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { Observation obs = new Observation(); obs.getSubject().setReference("Patient/P"); - myObservationDao.update(obs); + myObservationDao.create(obs); SearchParameterMap map = new SearchParameterMap(); map.setLoadSynchronous(true); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java index aa839c5ebde..1f47c6739d1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java @@ -697,7 +697,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test { myCaptureQueriesListener.logSelectQueries(); String selectQuery = myCaptureQueriesListener.getSelectQueries().get(1).getSql(true, true); - assertThat(selectQuery, containsString("HASH_VALUE=")); + assertThat(selectQuery, containsString("HASH_VALUE")); assertThat(selectQuery, not(containsString("HASH_SYS"))); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java index f44c2653b95..4d2d9d32827 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -398,7 +398,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertThat(unformattedSql, stringContainsInOrder( "IDX_STRING='Patient?identifier=urn%7C111'", - "HASH_SYS_AND_VALUE='-3122824860083758210'" + "HASH_SYS_AND_VALUE in ('-3122824860083758210')" )); assertThat(unformattedSql, not(containsString(("RES_DELETED_AT")))); assertThat(unformattedSql, not(containsString(("RES_TYPE")))); @@ -535,7 +535,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertThat(unformattedSql, stringContainsInOrder( "IDX_STRING='ServiceRequest?identifier=sys%7C111&patient=Patient%2F" + ptId.getIdPart() + "&performer=Practitioner%2F" + practId.getIdPart() + "'", - "HASH_SYS_AND_VALUE='6795110643554413877'" + "HASH_SYS_AND_VALUE in ('6795110643554413877')" )); assertThat(unformattedSql, not(containsString(("RES_DELETED_AT")))); assertThat(unformattedSql, not(containsString(("RES_TYPE")))); @@ -556,8 +556,8 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { assertThat(toUnqualifiedVersionlessIdValues(outcome), containsInAnyOrder(srId)); unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); assertThat(unformattedSql, stringContainsInOrder( - "SRC_PATH='ServiceRequest.subject.where(resolve() is Patient)'", - "SRC_PATH='ServiceRequest.performer'" + "SRC_PATH in ('ServiceRequest.subject.where(resolve() is Patient)')", + "SRC_PATH in ('ServiceRequest.performer')" )); assertThat(unformattedSql, not(containsString(("RES_DELETED_AT")))); assertThat(unformattedSql, not(containsString(("RES_TYPE")))); 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 baa846ac3f9..732b0e451ae 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 @@ -9,7 +9,10 @@ import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl; +import ca.uhn.fhir.jpa.term.TerminologyLoaderSvcLoincTest; +import ca.uhn.fhir.jpa.term.ZipCollectionBuilder; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; +import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; @@ -36,9 +39,11 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.ElementDefinition; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.IdType; @@ -49,11 +54,13 @@ import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r5.utils.IResourceValidator; import org.junit.jupiter.api.AfterEach; @@ -73,6 +80,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -95,6 +103,105 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { @Autowired private ValidationSettings myValidationSettings; + @Test + public void testValidateCodeInValueSetWithUnknownCodeSystem() { + myValidationSupport.fetchCodeSystem("http://not-exist"); // preload DefaultProfileValidationSupport + + ValueSet vs = new ValueSet(); + vs.setUrl("http://vs"); + vs + .getCompose() + .addInclude() + .setSystem("http://cs") + .addConcept(new ValueSet.ConceptReferenceComponent(new CodeType("code1"))) + .addConcept(new ValueSet.ConceptReferenceComponent(new CodeType("code2"))); + myValueSetDao.create(vs); + + StructureDefinition sd = new StructureDefinition(); + sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setType("Observation"); + sd.setUrl("http://sd"); + sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Observation"); + sd.getDifferential() + .addElement() + .setPath("Observation.value[x]") + .addType(new ElementDefinition.TypeRefComponent(new UriType("Quantity"))) + .setBinding(new ElementDefinition.ElementDefinitionBindingComponent().setStrength(Enumerations.BindingStrength.REQUIRED).setValueSet("http://vs")) + .setId("Observation.value[x]"); + myStructureDefinitionDao.create(sd); + + Observation obs = new Observation(); + obs.getMeta().addProfile("http://sd"); + obs.getText().setDivAsString("
Hello
"); + obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); + obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs"); + obs.getCode().setText("hello"); + obs.setSubject(new Reference("Patient/123")); + obs.addPerformer(new Reference("Practitioner/123")); + obs.setEffective(DateTimeType.now()); + obs.setStatus(ObservationStatus.FINAL); + + OperationOutcome oo; + + // Valid code + obs.setValue(new Quantity().setSystem("http://cs").setCode("code1").setValue(123)); + oo = validateAndReturnOutcome(obs); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); + assertEquals("No issues detected during validation", oo.getIssueFirstRep().getDiagnostics(), encode(oo)); + + // Invalid code + obs.setValue(new Quantity().setSystem("http://cs").setCode("code99").setValue(123)); + oo = validateAndReturnOutcome(obs); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); + assertEquals("Could not confirm that the codes provided are in the value set http://vs, and a code from this value set is required", oo.getIssueFirstRep().getDiagnostics(), encode(oo)); + + } + + @Test + public void testGenerateSnapshotOnStructureDefinitionWithNoBase() { + + // No base populated here, which isn't valid + StructureDefinition sd = new StructureDefinition(); + sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setUrl("http://sd"); + sd.getDifferential() + .addElement() + .setPath("Observation.value[x]") + .addType(new ElementDefinition.TypeRefComponent(new UriType("string"))) + .setId("Observation.value[x]"); + + try { + myStructureDefinitionDao.generateSnapshot(sd, null, null, null); + fail(); + } catch (PreconditionFailedException e) { + assertEquals("StructureDefinition[id=null, url=http://sd] has no base", e.getMessage()); + } + + myStructureDefinitionDao.create(sd); + + Observation obs = new Observation(); + obs.getMeta().addProfile("http://sd"); + obs.getText().setDivAsString("
Hello
"); + obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED); + obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs"); + obs.getCode().setText("hello"); + obs.setSubject(new Reference("Patient/123")); + obs.addPerformer(new Reference("Practitioner/123")); + obs.setEffective(DateTimeType.now()); + obs.setStatus(ObservationStatus.FINAL); + + OperationOutcome oo; + + // Valid code + obs.setValue(new Quantity().setSystem("http://cs").setCode("code1").setValue(123)); + try { + myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, null, mySrd); + fail(); + } catch (PreconditionFailedException e) { + assertEquals("StructureDefinition[id=null, url=http://sd] has no base", e.getMessage()); + } + } + /** * Use a valueset that explicitly brings in some UCUM codes */ @@ -505,6 +612,64 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } + + + @Test + public void testValidateValueSet() { + String input = "{\n" + + " \"resourceType\": \"ValueSet\",\n" + + " \"meta\": {\n" + + " \"profile\": [\n" + + " \"https://foo\"\n" + + " ]\n" + + " },\n" + + " \"text\": {\n" + + " \"status\": \"generated\",\n" + + " \"div\": \"
\"\n" + + " },\n" + + " \"url\": \"https://foo/bb\",\n" + + " \"name\": \"BBBehaviourType\",\n" + + " \"title\": \"BBBehaviour\",\n" + + " \"status\": \"draft\",\n" + + " \"version\": \"20190731\",\n" + + " \"experimental\": false,\n" + + " \"description\": \"alcohol habits.\",\n" + + " \"publisher\": \"BB\",\n" + + " \"immutable\": false,\n" + + " \"compose\": {\n" + + " \"include\": [\n" + + " {\n" + + " \"system\": \"https://bb\",\n" + + " \"concept\": [\n" + + " {\n" + + " \"code\": \"123\",\n" + + " \"display\": \"Current drinker\"\n" + + " },\n" + + " {\n" + + " \"code\": \"456\",\n" + + " \"display\": \"Ex-drinker\"\n" + + " },\n" + + " {\n" + + " \"code\": \"789\",\n" + + " \"display\": \"Lifetime non-drinker (finding)\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + ValueSet vs = myFhirCtx.newJsonParser().parseResource(ValueSet.class, input); + OperationOutcome oo = validateAndReturnOutcome(vs); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); + + assertEquals("The code 123 is not valid in the system https://bb", oo.getIssue().get(0).getDiagnostics()); + } + + + + + /** * Per: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/Handling.20incomplete.20CodeSystems *

@@ -533,16 +698,10 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { obs.getCode().getCodingFirstRep().setSystem("http://example.com/codesystem"); obs.getCode().getCodingFirstRep().setCode("foo-foo"); obs.getCode().getCodingFirstRep().setDisplay("Some Code"); - try { - outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome(); - ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); - fail(); - } catch (PreconditionFailedException e) { - OperationOutcome oo = (OperationOutcome) e.getOperationOutcome(); - ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo)); - assertEquals("None of the codes provided are in the value set http://example.com/valueset (http://example.com/valueset), and a code from this value set is required) (codes = http://example.com/codesystem#foo-foo)", oo.getIssueFirstRep().getDiagnostics()); - assertEquals(OperationOutcome.IssueSeverity.ERROR, oo.getIssueFirstRep().getSeverity()); - } + outcome = (OperationOutcome) myObservationDao.validate(obs, null, null, null, ValidationModeEnum.CREATE, "http://example.com/structuredefinition", mySrd).getOperationOutcome(); + ourLog.info("Outcome: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + assertEquals("Unknown code in fragment CodeSystem 'http://example.com/codesystem#foo-foo'", outcome.getIssueFirstRep().getDiagnostics()); + assertEquals(OperationOutcome.IssueSeverity.WARNING, outcome.getIssueFirstRep().getSeverity()); // Correct codesystem, Code in codesystem obs.getCode().getCodingFirstRep().setSystem("http://example.com/codesystem"); @@ -1442,4 +1601,20 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } + @Test + public void testKnownCodeSystemUnknownValueSetUri() { + CodeSystem cs = new CodeSystem(); + cs.setUrl(ITermLoaderSvc.LOINC_URI); + cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + cs.addConcept().setCode("10013-1"); + myCodeSystemDao.create(cs); + + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(new UriType("http://fooVs"), null, new StringType("10013-1"), new StringType(ITermLoaderSvc.LOINC_URI), null, null, null, mySrd); + + assertFalse(result.isOk()); + assertEquals("Unknown code {http://loinc.org}10013-1 - Unable to locate ValueSet[http://fooVs]", result.getMessage()); + } + + + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java index 1d751cc4833..1311279136b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java @@ -1,10 +1,10 @@ package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.CodeSystem; @@ -15,7 +15,6 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,14 +48,14 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { public void before02() throws IOException { ValueSet upload = loadResourceFromClasspath(ValueSet.class, "/extensional-case-3-vs.xml"); myExtensionalVsId = myValueSetDao.create(upload, mySrd).getId().toUnqualifiedVersionless(); - + CodeSystem upload2 = loadResourceFromClasspath(CodeSystem.class, "/extensional-case-3-cs.xml"); myCodeSystemDao.create(upload2, mySrd).getId().toUnqualifiedVersionless(); } @Test - public void testValidateCodeOperationByCodeAndSystemBad() { + public void testValidateCodeOperationNoValueSet() { UriType valueSetIdentifier = null; IdType id = null; CodeType code = new CodeType("8450-9-XXX"); @@ -64,22 +63,12 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType display = null; Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertFalse(result.isResult()); - } - - @Test - public void testValidateCodeOperationByCodeAndSystemGood() { - UriType valueSetIdentifier = null; - IdType id = null; - CodeType code = new CodeType("8450-9"); - UriType system = new UriType("http://acme.org"); - StringType display = null; - Coding coding = null; - CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); - assertEquals("Systolic blood pressure--expiration", result.getDisplay()); + try { + myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate.", e.getMessage()); + } } @Test @@ -91,8 +80,8 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType display = null; Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -105,9 +94,10 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType display = new StringType("Systolic blood pressure at First encounterXXXX"); Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertFalse(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertFalse(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); + assertEquals("Concept Display \"Systolic blood pressure at First encounterXXXX\" does not match expected \"Systolic blood pressure at First encounter\"", result.getMessage()); } @Test @@ -119,8 +109,8 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType display = new StringType("Systolic blood pressure at First encounter"); Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -134,8 +124,8 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { Coding coding = null; CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.addCoding().setSystem("http://acme.org").setCode("11378-7"); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -151,18 +141,18 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { Coding coding = null; CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.addCoding().setSystem("http://acme.org").setCode("11378-7"); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); myTerminologyDeferredStorageSvc.saveDeferred(); result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -175,8 +165,8 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType display = null; Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -191,18 +181,18 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType display = null; Coding coding = null; CodeableConcept codeableConcept = null; - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); myTerminologyDeferredStorageSvc.saveDeferred(); result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); result = myValueSetDao.validateCode(valueSetIdentifier, id, code, system, display, coding, codeableConcept, mySrd); - assertTrue(result.isResult()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -213,17 +203,17 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { ValueSet expanded = myValueSetDao.expand(myExtensionalVsId, null, mySrd); resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(expanded); ourLog.info(resp); - assertThat(resp, containsString("")); + assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); - assertThat(resp, containsString("")); + assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); - assertThat(resp, containsString("")); + assertThat(resp, containsString("")); assertThat(resp, containsString("")); assertThat(resp, containsString("")); @@ -236,12 +226,12 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { ourLog.info(resp); //@formatter:off assertThat(resp, stringContainsInOrder( - "", - "")); + "", + "")); //@formatter:on } - + @Test public void testExpandByValueSet_ExceedsMaxSize() { // Add a bunch of codes @@ -265,7 +255,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { } } - + @Test public void testValidateCodeAgainstBuiltInValueSetAndCodeSystemWithValidCode() { IPrimitiveType display = null; @@ -274,14 +264,14 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { StringType vsIdentifier = new StringType("http://hl7.org/fhir/ValueSet/administrative-gender"); StringType code = new StringType("male"); StringType system = new StringType("http://hl7.org/fhir/administrative-gender"); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(vsIdentifier, null, code, system, display, coding, codeableConcept, mySrd); - + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(vsIdentifier, null, code, system, display, coding, codeableConcept, mySrd); + ourLog.info(result.getMessage()); - assertTrue( result.isResult(), result.getMessage()); + assertTrue(result.isOk(), result.getMessage()); assertEquals("Male", result.getDisplay()); } - + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java index dce798cd173..43678936c0c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java @@ -1704,15 +1704,14 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { @Test public void testSearch_TagNotParam_SearchAllPartitions() { - IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code")); - IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code")); + IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code"), withIdentifier("http://foo", "bar")); + IIdType patientId1 = createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code"), withIdentifier("http://foo", "bar")); IIdType patientId2 = createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code")); createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code2")); createPatient(withPartition(1), withActiveTrue(), withTag("http://system", "code2")); createPatient(withPartition(2), withActiveTrue(), withTag("http://system", "code2")); addReadAllPartitions(); - myCaptureQueriesListener.clear(); SearchParameterMap map = new SearchParameterMap(); map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT)); @@ -1725,6 +1724,26 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { ourLog.info("Search SQL:\n{}", searchSql); assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'")); + + // And with another param + + addReadAllPartitions(); + myCaptureQueriesListener.clear(); + map = new SearchParameterMap(); + map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT)); + map.add(Patient.SP_IDENTIFIER, new TokenParam("http://foo", "bar")); + map.setLoadSynchronous(true); + results = myPatientDao.search(map); + ids = toUnqualifiedVersionlessIds(results); + assertThat(ids, Matchers.contains(patientIdNull, patientId1)); + + searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + ourLog.info("Search SQL:\n{}", searchSql); + assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID")); + assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'")); + assertEquals(1, StringUtils.countMatches(searchSql, "myparamsto1_.HASH_SYS_AND_VALUE in")); + + } @Test @@ -1947,10 +1966,10 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertThat(ids, Matchers.contains(observationId)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); ourLog.info("Search SQL:\n{}", searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.PARTITION_ID='1'")); - assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.SRC_PATH='Observation.subject'")); + assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.SRC_PATH in ('Observation.subject')")); assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.TARGET_RESOURCE_ID='" + patientId.getIdPartAsLong() + "'")); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); @@ -1985,10 +2004,10 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest { myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertThat(ids, Matchers.contains(observationId)); - String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true); + String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); ourLog.info("Search SQL:\n{}", searchSql); assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.PARTITION_ID is null")); - assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.SRC_PATH='Observation.subject'")); + assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.SRC_PATH in ('Observation.subject')")); assertEquals(1, StringUtils.countMatches(searchSql, "myresource1_.TARGET_RESOURCE_ID='" + patientId.getIdPartAsLong() + "'")); assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID")); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java index 3d13c980128..35f439fba64 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchWithInterceptorR4Test.java @@ -68,7 +68,7 @@ public class SearchWithInterceptorR4Test extends BaseJpaR4Test { String query = list.get(0).getSql(true, false); ourLog.info("Query: {}", query); - assertThat(query, containsString("HASH_SYS_AND_VALUE='3788488238034018567'")); + assertThat(query, containsString("HASH_SYS_AND_VALUE in ('3788488238034018567')")); } finally { myInterceptorRegistry.unregisterInterceptor(interceptor); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/graphql/JpaStorageServicesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/graphql/JpaStorageServicesTest.java index 01bb7db42dd..dd0c5d8f626 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/graphql/JpaStorageServicesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/graphql/JpaStorageServicesTest.java @@ -9,6 +9,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Appointment; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.utilities.graphql.Argument; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.hl7.fhir.utilities.graphql.StringValue; @@ -22,6 +23,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -94,4 +96,29 @@ public class JpaStorageServicesTest extends BaseJpaR4Test { assertEquals("Unknown GraphQL argument \"test\". Value GraphQL argument for this type are: [_id, _language, actor, appointment_type, based_on, date, identifier, location, part_status, patient, practitioner, reason_code, reason_reference, service_category, service_type, slot, specialty, status, supporting_info]", e.getMessage()); } } + + private void createSomePatientWithId(String id) { + Patient somePatient = new Patient(); + somePatient.setId(id); + myPatientDao.update(somePatient); + } + + @Test + public void testListResourceGraphqlArrayOfArgument() { + createSomePatientWithId("hapi-123"); + createSomePatientWithId("hapi-124"); + + Argument argument = new Argument(); + argument.setName("_id"); + argument.addValue(new StringValue("hapi-123")); + argument.addValue(new StringValue("hapi-124")); + + List result = new ArrayList<>(); + mySvc.listResources(mySrd, "Patient", Collections.singletonList(argument), result); + + assertFalse(result.isEmpty()); + + List expectedId = Arrays.asList("hapi-123", "hapi-124"); + assertTrue(result.stream().allMatch((it) -> expectedId.contains(it.getIdElement().getIdPart()))); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java index cf57cdcbd95..df5796409d6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/IgInstallerDstu3Test.java @@ -98,12 +98,12 @@ public class IgInstallerDstu3Test extends BaseJpaDstu3Test { PackageInstallationSpec spec = new PackageInstallationSpec() .setName("nictiz.fhir.nl.stu3.questionnaires") .setVersion("1.0.2") - .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) + .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY) .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")); + assertThat(outcome.getMessage(), hasItem("Indexing StructureDefinition 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()); @@ -207,6 +207,6 @@ public class IgInstallerDstu3Test extends BaseJpaDstu3Test { 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)); + igInstaller.install(new PackageInstallationSpec().setName("basisprofil.de").setVersion("0.2.40").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); } } 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 index 80557cc84bf..2dfd045bfa3 100644 --- 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 @@ -94,6 +94,7 @@ public class NpmTestR4 extends BaseJpaR4Test { myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); } + @Test public void testCacheDstu3Package() throws Exception { byte[] bytes = loadClasspathBytes("/packages/nictiz.fhir.nl.stu3.questionnaires-1.0.2.tgz"); @@ -215,6 +216,10 @@ public class NpmTestR4 extends BaseJpaR4Test { NpmPackage pkg = myPackageCacheManager.loadPackage("UK.Core.r4", "1.1.0"); assertEquals(null, pkg.description()); assertEquals("UK.Core.r4", pkg.name()); + + // Ensure that we loaded the contents + IBundleProvider searchResult = myStructureDefinitionDao.search(SearchParameterMap.newSynchronous("url", new UriParam("https://fhir.nhs.uk/R4/StructureDefinition/UKCore-Patient"))); + assertEquals(1, searchResult.sizeOrThrowNpe()); } @Test @@ -262,7 +267,7 @@ public class NpmTestR4 extends BaseJpaR4Test { 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")); + assertThat(outcome.getMessage(), hasItem("Indexing CodeSystem 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); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java index fb8ac5800c5..9c078cb6ae5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/JpaGraphQLR4ProviderTest.java @@ -31,6 +31,7 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.utilities.graphql.Argument; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; +import org.hl7.fhir.utilities.graphql.Value; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -38,8 +39,11 @@ import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.startsWith; @@ -158,7 +162,38 @@ public class JpaGraphQLR4ProviderTest { " }]\n" + " },{\n" + " \"name\":[{\n" + - " \"given\":[\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" + + " \"given\":[\"pet\",\"GivenOnlyB1\",\"GivenOnlyB2\"]\n" + + " }]\n" + + " }]\n" + + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); + assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @Test + public void testGraphSystemArrayArgumentList() throws Exception { + String query = "{PatientList(id:[\"hapi-123\",\"hapi-124\"]){id,name{family}}}"; + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$graphql?query=" + UrlUtil.escapeUrlParam(query)); + CloseableHttpResponse status = ourClient.execute(httpGet); + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + + assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" + + " \"PatientList\":[{\n" + + " \"id\":\"Patient/hapi-123/_history/2\",\n" + + " \"name\":[{\n" + + " \"family\":\"FAMILY 123\"\n" + + " }]\n" + + " },{\n" + + " \"id\":\"Patient/hapi-124/_history/1\",\n" + + " \"name\":[{\n" + + " \"family\":\"FAMILY 124\"\n" + " }]\n" + " }]\n" + "}" + DATA_SUFFIX), TestUtil.stripWhitespace(responseContent)); @@ -230,24 +265,46 @@ public class JpaGraphQLR4ProviderTest { ourLog.info("listResources of {} - {}", theType, theSearchParams); if (theSearchParams.size() == 1) { - String name = theSearchParams.get(0).getName(); - if ("name".equals(name)) { - Patient p = new Patient(); - p.addName() - .setFamily(theSearchParams.get(0).getValues().get(0).toString()) + Argument argument = theSearchParams.get(0); + + String name = argument.getName(); + List value = argument.getValues().stream() + .map((it) -> it.getValue()) + .collect(Collectors.toList()); + + if ("name".equals(name) && "pet".equals(value.get(0))) { + Patient patient1 = new Patient(); + patient1.addName() + .setFamily("pet") .addGiven("GIVEN1") .addGiven("GIVEN2"); - p.addName() + patient1.addName() .addGiven("GivenOnly1") .addGiven("GivenOnly2"); - theMatches.add(p); - p = new Patient(); - p.addName() + Patient patient2 = new Patient(); + patient2.addName() + .addGiven("pet") .addGiven("GivenOnlyB1") .addGiven("GivenOnlyB2"); - theMatches.add(p); + theMatches.add(patient1); + theMatches.add(patient2); + } + + if ("id".equals(name) && Arrays.asList("hapi-123", "hapi-124").containsAll(value)) { + Patient patient1 = new Patient(); + patient1.setId("Patient/hapi-123/_history/2"); + patient1.addName() + .setFamily("FAMILY 123"); + + Patient patient2 = new Patient(); + patient2.setId("Patient/hapi-124/_history/1"); + patient2.addName() + .setFamily("FAMILY 124"); + + theMatches.add(patient1); + theMatches.add(patient2); } } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java index 5d2a153e95a..c577f3abc27 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2ValueSetTest.java @@ -45,23 +45,7 @@ public class ResourceProviderDstu2ValueSetTest extends BaseResourceProviderDstu2 .operation() .onInstance(myExtensionalVsId) .named("validate-code") - .withParameter(Parameters.class, "code", new CodeDt("8495-4")) - .andParameter("system", new UriDt("http://loinc.org")) - .execute(); - - String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); - ourLog.info(resp); - - assertEquals(new BooleanDt(true), respParam.getParameter().get(0).getValue()); - } - - @Test - public void testValidateCodeOperationByCodeAndSystemType() { - Parameters respParam = ourClient - .operation() - .onType(ValueSet.class) - .named("validate-code") - .withParameter(Parameters.class, "code", new CodeDt("8450-9")) + .withParameter(Parameters.class, "code", new CodeDt("11378-7")) .andParameter("system", new UriDt("http://loinc.org")) .execute(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java index 882e7365637..9c85d3f9afb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java @@ -749,6 +749,7 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 .named("validate-code") .withParameter(Parameters.class, "code", new CodeType("8450-9")) .andParameter("system", new UriType("http://acme.org")) + .andParameter("url", new UriType("http://www.healthintersections.com.au/fhir/ValueSet/extensional-case-2")) .execute(); String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); @@ -780,11 +781,8 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 assertEquals("result", respParam.getParameter().get(0).getName()); assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue()); - assertEquals("message", respParam.getParameter().get(1).getName()); - assertThat(((StringType) respParam.getParameter().get(1).getValue()).getValue(), Matchers.containsStringIgnoringCase("succeeded")); - - assertEquals("display", respParam.getParameter().get(2).getName()); - assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue()); } /** @@ -810,11 +808,8 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 assertEquals("result", respParam.getParameter().get(0).getName()); assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue()); - assertEquals("message", respParam.getParameter().get(1).getName()); - assertThat(((StringType) respParam.getParameter().get(1).getValue()).getValue(), Matchers.containsStringIgnoringCase("succeeded")); - - assertEquals("display", respParam.getParameter().get(2).getName()); - assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue()); } @AfterEach diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index d6393c27fe1..7d6b826e16a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -202,10 +202,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { return names; } - protected void waitForActivatedSubscriptionCount(int theSize) throws Exception { - TestUtil.waitForSize(theSize, () -> mySubscriptionRegistry.size()); - Thread.sleep(500); - } @AfterAll public static void afterClassClearContextBaseResourceProviderR4Test() throws Exception { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java index 69a3f9fc44f..7d6767f1aa4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java @@ -10,9 +10,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.entity.TermValueSetPreExpansionStatusEnum; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; @@ -54,6 +54,7 @@ import java.util.Optional; import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM; import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest.URL_MY_VALUE_SET; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsStringIgnoringCase; import static org.hamcrest.Matchers.not; @@ -61,7 +62,6 @@ import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -98,11 +98,11 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { private void persistCodeSystem(CodeSystem theCodeSystem) { new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(@Nonnull TransactionStatus theStatus) { - myExtensionalCsId = myCodeSystemDao.create(theCodeSystem, mySrd).getId().toUnqualifiedVersionless(); - } - }); + @Override + protected void doInTransactionWithoutResult(@Nonnull TransactionStatus theStatus) { + myExtensionalCsId = myCodeSystemDao.create(theCodeSystem, mySrd).getId().toUnqualifiedVersionless(); + } + }); myCodeSystemDao.readEntity(myExtensionalCsId, null).getId(); } @@ -156,7 +156,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { createLocalVsWithUnknownCode(codeSystem); } - private void createLocalCsAndVs() { + private void createLocalCs() { CodeSystem codeSystem = new CodeSystem(); codeSystem.setUrl(URL_MY_CODE_SYSTEM); codeSystem.setContent(CodeSystemContentMode.COMPLETE); @@ -671,8 +671,8 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { .named("$expand") .withNoParameters(Parameters.class) .returnResourceType(ValueSet.class) - .execute(); - ourLog.info("Expanded: {}",myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); + .execute(); + ourLog.info("Expanded: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); assertEquals(1, expanded.getExpansion().getContains().size()); // Update the CodeSystem URL and Codes @@ -696,12 +696,11 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { .withNoParameters(Parameters.class) .returnResourceType(ValueSet.class) .execute(); - ourLog.info("Expanded: {}",myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); + ourLog.info("Expanded: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); assertEquals(1, expanded.getExpansion().getContains().size()); } - /** * #516 */ @@ -796,7 +795,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } private void validateTermValueSetNotExpanded(String theValueSetName) { - runInTransaction(()->{ + runInTransaction(() -> { Optional optionalValueSetByResourcePid = myTermValueSetDao.findByResourcePid(myExtensionalVsIdOnResourceTable); assertTrue(optionalValueSetByResourcePid.isPresent()); @@ -814,7 +813,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } private void validateTermValueSetExpandedAndChildren(String theValueSetName, CodeSystem theCodeSystem) { - runInTransaction(()->{ + runInTransaction(() -> { Optional optionalValueSetByResourcePid = myTermValueSetDao.findByResourcePid(myExtensionalVsIdOnResourceTable); assertTrue(optionalValueSetByResourcePid.isPresent()); @@ -906,10 +905,11 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { @Test public void testValidateCodeOperationByCodeAndSystemInstanceOnType() throws IOException { - createLocalCsAndVs(); + createLocalCs(); + createLocalVsWithIncludeConcept(); String url = ourServerBase + - "/ValueSet/$validate-code?system=" + + "/ValueSet/" + myLocalValueSetId.getIdPart() + "/$validate-code?system=" + UrlUtil.escapeUrlParam(URL_MY_CODE_SYSTEM) + "&code=AA"; @@ -926,7 +926,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { @Test public void testValidateCodeOperationByCodeAndSystemInstanceOnInstance() throws IOException { - createLocalCsAndVs(); + createLocalCs(); createLocalVsWithIncludeConcept(); String url = ourServerBase + @@ -953,7 +953,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { Parameters respParam = myClient .operation() - .onType(ValueSet.class) + .onInstance(myExtensionalVsId) .named("validate-code") .withParameter(Parameters.class, "code", new CodeType("8450-9")) .andParameter("system", new UriType("http://acme.org")) @@ -965,6 +965,24 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue()); } + @Test + public void testValidateCodeOperationNoValueSetProvided() throws Exception { + loadAndPersistCodeSystemAndValueSet(); + + try { + myClient + .operation() + .onType(ValueSet.class) + .named("validate-code") + .withParameter(Parameters.class, "code", new CodeType("8450-9")) + .andParameter("system", new UriType("http://acme.org")) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate.", e.getMessage()); + } + } + @Test public void testValidateCodeAgainstBuiltInSystem() { Parameters respParam = myClient @@ -983,11 +1001,8 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { assertEquals("result", respParam.getParameter().get(0).getName()); assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue()); - assertEquals("message", respParam.getParameter().get(1).getName()); - assertThat(((StringType) respParam.getParameter().get(1).getValue()).getValue(), containsStringIgnoringCase("succeeded")); - - assertEquals("display", respParam.getParameter().get(2).getName()); - assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue()); } @Test @@ -1018,7 +1033,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { ourLog.info("Response: {}", response); } - HttpGet validateCodeGet = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?code=ChildAA&_pretty=true"); + HttpGet validateCodeGet = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?system=http://mycs&code=ChildAA&_pretty=true"); try (CloseableHttpResponse status = ourHttpClient.execute(validateCodeGet)) { String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response: {}", response); @@ -1026,7 +1041,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { assertEquals(true, output.getParameterBool("result")); } - HttpGet validateCodeGet2 = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?code=FOO&_pretty=true"); + HttpGet validateCodeGet2 = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?system=http://mycs&code=FOO&_pretty=true"); try (CloseableHttpResponse status = ourHttpClient.execute(validateCodeGet2)) { String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response: {}", response); @@ -1070,7 +1085,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { TermConcept parentB = new TermConcept(cs, "ParentB").setDisplay("Parent B"); cs.getConcepts().add(parentB); - theTermCodeSystemStorageSvc.storeNewCodeSystemVersion(new ResourcePersistentId(table.getId()), URL_MY_CODE_SYSTEM, "SYSTEM NAME", "SYSTEM VERSION" , cs, table); + theTermCodeSystemStorageSvc.storeNewCodeSystemVersion(new ResourcePersistentId(table.getId()), URL_MY_CODE_SYSTEM, "SYSTEM NAME", "SYSTEM VERSION", cs, table); return codeSystem; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java index a991d221e60..0f24f202834 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/BaseResourceProviderR5Test.java @@ -205,23 +205,6 @@ public abstract class BaseResourceProviderR5Test extends BaseJpaR5Test { return names; } - protected void waitForActivatedSubscriptionCount(int theSize) throws Exception { - for (int i = 0; ; i++) { - if (i == 10) { - fail("Failed to init subscriptions"); - } - try { - mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); - break; - } catch (ResourceVersionConflictException e) { - Thread.sleep(250); - } - } - - TestUtil.waitForSize(theSize, () -> mySubscriptionRegistry.size()); - Thread.sleep(500); - } - @AfterAll public static void afterClassClearContextBaseResourceProviderR5Test() throws Exception { JettyUtil.closeServer(ourServer); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java index b81d7fb8c0f..919c3b8e8b9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java @@ -10,10 +10,10 @@ 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.entity.TermValueSetPreExpansionStatusEnum; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; @@ -53,6 +53,7 @@ import java.util.Optional; import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM; import static ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest.URL_MY_VALUE_SET; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsStringIgnoringCase; import static org.hamcrest.Matchers.not; @@ -60,7 +61,6 @@ import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -983,8 +983,8 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { .named("$expand") .withNoParameters(Parameters.class) .returnResourceType(ValueSet.class) - .execute(); - ourLog.info("Expanded: {}",myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); + .execute(); + ourLog.info("Expanded: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); assertEquals(1, expanded.getExpansion().getContains().size()); // Update the CodeSystem URL and Codes @@ -1008,12 +1008,11 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { .withNoParameters(Parameters.class) .returnResourceType(ValueSet.class) .execute(); - ourLog.info("Expanded: {}",myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); + ourLog.info("Expanded: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expanded)); assertEquals(1, expanded.getExpansion().getContains().size()); } - /** * #516 */ @@ -1108,7 +1107,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { } private void validateTermValueSetNotExpanded(String theValueSetName) { - runInTransaction(()->{ + runInTransaction(() -> { Optional optionalValueSetByResourcePid = myTermValueSetDao.findByResourcePid(myExtensionalVsIdOnResourceTable); assertTrue(optionalValueSetByResourcePid.isPresent()); @@ -1126,7 +1125,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { } private void validateTermValueSetExpandedAndChildren(String theValueSetName, CodeSystem theCodeSystem) { - runInTransaction(()->{ + runInTransaction(() -> { Optional optionalValueSetByResourcePid = myTermValueSetDao.findByResourcePid(myExtensionalVsIdOnResourceTable); assertTrue(optionalValueSetByResourcePid.isPresent()); @@ -1219,9 +1218,10 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { @Test public void testValidateCodeOperationByCodeAndSystemInstanceOnType() throws IOException { createLocalCsAndVs(); + createLocalVsWithIncludeConcept(); String url = ourServerBase + - "/ValueSet/$validate-code?system=" + + "/ValueSet/" + myLocalValueSetId.getIdPart() + "/$validate-code?system=" + UrlUtil.escapeUrlParam(URL_MY_CODE_SYSTEM) + "&code=AA"; @@ -1265,7 +1265,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { Parameters respParam = myClient .operation() - .onType(ValueSet.class) + .onInstance(myExtensionalVsId) .named("validate-code") .withParameter(Parameters.class, "code", new CodeType("8450-9")) .andParameter("system", new UriType("http://acme.org")) @@ -1297,12 +1297,10 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { assertEquals("result", respParam.getParameter().get(0).getName()); assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue()); - assertEquals("message", respParam.getParameter().get(1).getName()); - assertThat(((StringType) respParam.getParameter().get(1).getValue()).getValue(), containsStringIgnoringCase("succeeded")); - - assertEquals("display", respParam.getParameter().get(2).getName()); - assertEquals("Male", ((StringType) respParam.getParameter().get(2).getValue()).getValue()); + assertEquals("display", respParam.getParameter().get(1).getName()); + assertEquals("Male", ((StringType) respParam.getParameter().get(1).getValue()).getValue()); } + // Good code and system, but not in specified valueset { Parameters respParam = myClient @@ -1322,7 +1320,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { assertEquals(false, ((BooleanType) respParam.getParameter().get(0).getValue()).getValue()); assertEquals("message", respParam.getParameter().get(1).getName()); - assertThat(((StringType) respParam.getParameter().get(1).getValue()).getValue(), containsStringIgnoringCase("Code not found")); + assertEquals("Unknown code 'http://hl7.org/fhir/administrative-gender#male'", ((StringType) respParam.getParameter().get(1).getValue()).getValue()); } } @@ -1352,9 +1350,10 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { try (CloseableHttpResponse status = ourHttpClient.execute(expandGet)) { String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response: {}", response); + assertThat(response, containsString("")); } - HttpGet validateCodeGet = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?code=ChildAA&_pretty=true"); + HttpGet validateCodeGet = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?system=http://mycs&code=ChildAA&_pretty=true"); try (CloseableHttpResponse status = ourHttpClient.execute(validateCodeGet)) { String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response: {}", response); @@ -1362,7 +1361,32 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { assertEquals(true, output.getParameterBool("result")); } - HttpGet validateCodeGet2 = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?code=FOO&_pretty=true"); + HttpGet validateCodeGet2 = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?system=http://mycs&code=FOO&_pretty=true"); + try (CloseableHttpResponse status = ourHttpClient.execute(validateCodeGet2)) { + String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info("Response: {}", response); + Parameters output = myFhirCtx.newXmlParser().parseResource(Parameters.class, response); + assertEquals(false, output.getParameterBool("result")); + } + + // Now do a pre-expansion + myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); + + expandGet = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$expand?_pretty=true"); + try (CloseableHttpResponse status = ourHttpClient.execute(expandGet)) { + String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info("Response: {}", response); + } + + validateCodeGet = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?system=http://mycs&code=ChildAA&_pretty=true"); + try (CloseableHttpResponse status = ourHttpClient.execute(validateCodeGet)) { + String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info("Response: {}", response); + Parameters output = myFhirCtx.newXmlParser().parseResource(Parameters.class, response); + assertEquals(true, output.getParameterBool("result")); + } + + validateCodeGet2 = new HttpGet(ourServerBase + "/ValueSet/" + vsId.getIdPart() + "/$validate-code?system=http://mycs&code=FOO&_pretty=true"); try (CloseableHttpResponse status = ourHttpClient.execute(validateCodeGet2)) { String response = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response: {}", response); @@ -1407,7 +1431,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { TermConcept parentB = new TermConcept(cs, "ParentB").setDisplay("Parent B"); cs.getConcepts().add(parentB); - theTermCodeSystemStorageSvc.storeNewCodeSystemVersion(new ResourcePersistentId(table.getId()), URL_MY_CODE_SYSTEM, "SYSTEM NAME", "SYSTEM VERSION" , cs, table); + theTermCodeSystemStorageSvc.storeNewCodeSystemVersion(new ResourcePersistentId(table.getId()), URL_MY_CODE_SYSTEM, "SYSTEM NAME", "SYSTEM VERSION", cs, table); return codeSystem; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java index 667bb318d44..7c7e0d09b81 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java @@ -508,7 +508,7 @@ public class LastNElasticsearchSvcMultipleObservationsIT { for (int entryCount = 0; entryCount < 10; entryCount++) { ObservationJson observationJson = new ObservationJson(); - String identifier = String.valueOf((entryCount + patientCount * 10)); + String identifier = String.valueOf((entryCount + patientCount * 10L)); observationJson.setIdentifier(identifier); observationJson.setSubject(subject); @@ -524,7 +524,7 @@ public class LastNElasticsearchSvcMultipleObservationsIT { assertTrue(elasticsearchSvc.createOrUpdateObservationCodeIndex(codeableConceptId2, codeJson2)); } - Date effectiveDtm = new Date(baseObservationDate.getTimeInMillis() - ((10 - entryCount) * 3600 * 1000)); + Date effectiveDtm = new Date(baseObservationDate.getTimeInMillis() - ((10L - entryCount) * 3600L * 1000L)); observationJson.setEffectiveDtm(effectiveDtm); assertTrue(elasticsearchSvc.createOrUpdateObservationIndex(identifier, observationJson)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java index a9818ecac0d..e8562ad45d4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java @@ -86,4 +86,7 @@ public class SubscriptionTestUtil { subscriber.setEmailSender(myEmailSender); } + public int getActiveSubscriptionCount() { + return mySubscriptionRegistry.size(); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java index 6bb50856b2e..fa642b17864 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java @@ -157,6 +157,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); + waitForActivatedSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); @@ -169,7 +170,6 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { ourLog.info("Current interceptors:\n * {}", allInterceptors); // Should see 1 subscription notification - waitForQueueToDrain(); waitForSize(0, ourCreatedObservations); waitForSize(1, ourUpdatedObservations); @@ -181,20 +181,21 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); waitForQueueToDrain(); + ourLog.info("Have {} updates and {} subscriptions - sending observation", ourUpdatedObservations.size(), mySubscriptionTestUtil.getActiveSubscriptionCount()); Observation observation2 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); // Should see one subscription notification waitForSize(0, ourCreatedObservations); waitForSize(3, ourUpdatedObservations); - // Delet one subscription + // Delete one subscription ourClient.delete().resourceById(new IdDt("Subscription/" + subscription2.getId())).execute(); + waitForActivatedSubscriptionCount(1); + ourLog.info("Have {} updates and {} subscriptions - sending observation", ourUpdatedObservations.size(), mySubscriptionTestUtil.getActiveSubscriptionCount()); Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); // Should see only one subscription notification - waitForQueueToDrain(); waitForSize(0, ourCreatedObservations); waitForSize(4, ourUpdatedObservations); @@ -204,6 +205,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { CodingDt coding = codeableConcept.addCoding(); coding.setCode(code + "111"); coding.setSystem("SNOMED-CT"); + ourLog.info("Have {} updates and {} subscriptions - sending observation", ourUpdatedObservations.size(), mySubscriptionTestUtil.getActiveSubscriptionCount()); ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute(); // Should see no subscription notification @@ -218,6 +220,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { CodingDt coding1 = codeableConcept1.addCoding(); coding1.setCode(code); coding1.setSystem("SNOMED-CT"); + ourLog.info("Have {} updates and {} subscriptions - sending observation", ourUpdatedObservations.size(), mySubscriptionTestUtil.getActiveSubscriptionCount()); ourClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); // Should see only one subscription notification diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java index 4a547aa3eb8..ed435066e35 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.HapiExtensions; +import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; @@ -250,7 +251,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { // Manually unregister all subscriptions mySubscriptionRegistry.unregisterAllSubscriptions(); - waitForActivatedSubscriptionCount(0); + assertEquals(0, mySubscriptionRegistry.size()); // Force a reload mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java index ac3759053f1..904b0b1653f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java @@ -21,9 +21,7 @@ import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.test.utilities.JettyUtil; -import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Lists; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -42,7 +40,6 @@ import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; /** * Test the rest-hook subscriptions @@ -90,23 +87,6 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B mySubscriptionTestUtil.waitForQueueToDrain(); } - protected void waitForActivatedSubscriptionCount(int theSize) throws Exception { - for (int i = 0; ; i++) { - if (i == 10) { - fail("Failed to init subscriptions"); - } - try { - mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); - break; - } catch (ResourceVersionConflictException e) { - Thread.sleep(250); - } - } - - TestUtil.waitForSize(theSize, () -> mySubscriptionRegistry.size()); - Thread.sleep(500); - } - private Subscription createSubscription(String criteria, String payload, String endpoint) throws InterruptedException { Subscription subscription = new Subscription(); subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); @@ -232,8 +212,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); - runInTransaction(()->{ - ourLog.info("All token indexes:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t->t.toString()).collect(Collectors.joining("\n * "))); + runInTransaction(() -> { + ourLog.info("All token indexes:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); }); myCaptureQueriesListener.clear(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java index 711bdb8f4f7..b228a31cfa6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java @@ -2,10 +2,8 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; -import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Lists; import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.Coding; @@ -13,10 +11,10 @@ import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.PrimitiveType; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.Type; +import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -30,12 +28,12 @@ import java.util.Set; import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.greaterThan; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { @@ -228,11 +226,13 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { ZipCollectionBuilder files = new ZipCollectionBuilder(); TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); myLoader.loadLoinc(files.getFiles(), mySrd); + myTerminologyDeferredStorageSvc.saveDeferred(); + myTerminologyDeferredStorageSvc.saveDeferred(); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(null, null, new StringType("10013-1"), new StringType(ITermLoaderSvc.LOINC_URI), null, null, null, mySrd); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(new UriType("http://loinc.org/vs"), null, new StringType("10013-1"), new StringType(ITermLoaderSvc.LOINC_URI), null, null, null, mySrd); - assertTrue(result.isResult()); - assertEquals("Found code", result.getMessage()); + assertTrue(result.isOk()); + assertEquals("R' wave amplitude in lead I", result.getDisplay()); } @Test @@ -240,11 +240,13 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { ZipCollectionBuilder files = new ZipCollectionBuilder(); TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); myLoader.loadLoinc(files.getFiles(), mySrd); + myTerminologyDeferredStorageSvc.saveDeferred(); + myTerminologyDeferredStorageSvc.saveDeferred(); - IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(null, null, new StringType("10013-1-9999999999"), new StringType(ITermLoaderSvc.LOINC_URI), null, null, null, mySrd); + IValidationSupport.CodeValidationResult result = myValueSetDao.validateCode(new UriType("http://loinc.org/vs"), null, new StringType("10013-1-9999999999"), new StringType(ITermLoaderSvc.LOINC_URI), null, null, null, mySrd); - assertFalse(result.isResult()); - assertEquals("Code not found", result.getMessage()); + assertFalse(result.isOk()); + assertEquals("Unknown code {http://loinc.org}10013-1-9999999999 - Unable to expand ValueSet[http://loinc.org/vs]", result.getMessage()); } private Set toExpandedCodes(ValueSet theExpanded) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java index 6467dbcb19d..d0ff1cebb37 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java @@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.model.TranslationRequest; import ca.uhn.fhir.jpa.entity.TermConceptMap; import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; @@ -43,8 +42,8 @@ import static org.junit.jupiter.api.Assertions.fail; public class TerminologySvcImplR4Test extends BaseTermR4Test { private static final Logger ourLog = LoggerFactory.getLogger(TerminologySvcImplR4Test.class); - ValidationOptions optsNoGuess = new ValidationOptions(); - ValidationOptions optsGuess = new ValidationOptions().guessSystem(); + ConceptValidationOptions optsNoGuess = new ConceptValidationOptions(); + ConceptValidationOptions optsGuess = new ConceptValidationOptions().setInferSystem(true); private IIdType myConceptMapId; private void createAndPersistConceptMap() { @@ -1799,42 +1798,37 @@ public class TerminologySvcImplR4Test extends BaseTermR4Test { myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); - IFhirResourceDaoValueSet.ValidateCodeResult result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, null, null); + IValidationSupport.CodeValidationResult result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, null, null); assertNull(result); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, "BOGUS", null, null, null); - assertNull(result); + assertFalse(result.isOk()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, "11378-7", null, null, null); - assertNull(result); + assertFalse(result.isOk()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsGuess, valueSet, null, "11378-7", null, null, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsGuess, valueSet, null, "11378-7", "Systolic blood pressure at First encounter", null, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, "http://acme.org", "11378-7", null, null, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); Coding coding = new Coding("http://acme.org", "11378-7", "Systolic blood pressure at First encounter"); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, coding, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.addCoding(new Coding("BOGUS", "BOGUS", "BOGUS")); codeableConcept.addCoding(coding); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, null, codeableConcept); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } @@ -1852,43 +1846,38 @@ public class TerminologySvcImplR4Test extends BaseTermR4Test { myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); - IFhirResourceDaoValueSet.ValidateCodeResult result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, null, null); + IValidationSupport.CodeValidationResult result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, null, null); assertNull(result); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, "BOGUS", null, null, null); - assertNull(result); + assertFalse(result.isOk()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, "11378-7", null, null, null); - assertNull(result); + assertFalse(result.isOk()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsGuess, valueSet, null, "11378-7", null, null, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsGuess, valueSet, null, "11378-7", "Systolic blood pressure at First encounter", null, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, "http://acme.org", "11378-7", null, null, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); Coding coding = new Coding("http://acme.org", "11378-7", "Systolic blood pressure at First encounter"); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, coding, null); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.addCoding(new Coding("BOGUS", "BOGUS", "BOGUS")); codeableConcept.addCoding(coding); result = myTermSvc.validateCodeIsInPreExpandedValueSet(optsNoGuess, valueSet, null, null, null, null, codeableConcept); - assertTrue(result.isResult()); - assertEquals("Validation succeeded", result.getMessage()); + assertTrue(result.isOk()); assertEquals("Systolic blood pressure at First encounter", result.getDisplay()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/resources/dstu3/nl/LandISOCodelijst-2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000.json b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/nl/LandISOCodelijst-2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000.json new file mode 100755 index 00000000000..ff5aad34392 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/nl/LandISOCodelijst-2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000.json @@ -0,0 +1,55 @@ +{ + "resourceType": "ValueSet", + "id": "2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/shareablevalueset" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/resource-effectivePeriod", + "valuePeriod": { + "start": "2017-12-31T00:00:00+02:00" + } + } + ], + "url": "http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2--20171231000000", + "identifier": [ + { + "use": "official", + "system": "http://art-decor.org/ns/oids/vs", + "value": "2.16.840.1.113883.2.4.3.11.60.40.2.20.5.2" + } + ], + "version": "2017-12-31T00:00:00", + "name": "LandISOCodelijst", + "title": "LandISOCodelijst", + "status": "active", + "experimental": false, + "publisher": "Registratie aan de bron", + "contact": [ + { + "name": "Registratie aan de bron", + "telecom": [ + { + "system": "url", + "value": "https://www.registratieaandebron.nl" + }, + { + "system": "url", + "value": "https://zibs.nl" + } + ] + } + ], + "description": "ISO 3166-1 (alpha-2) - Alle waarden", + "immutable": false, + "compose": { + "include": [ + { + "system": "urn:iso:std:iso:3166" + } + ] + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/resources/extensional-case-2.xml b/hapi-fhir-jpaserver-base/src/test/resources/extensional-case-2.xml index fb5ac110c50..4ba16d57758 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/extensional-case-2.xml +++ b/hapi-fhir-jpaserver-base/src/test/resources/extensional-case-2.xml @@ -26,6 +26,10 @@ + + + + @@ -124,4 +128,4 @@ - \ No newline at end of file + diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java index 5e9b5084898..c640215f608 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java @@ -74,7 +74,11 @@ public class EmpiResourceDaoSvc { } public DaoMethodOutcome updatePerson(IAnyResource thePerson) { - return myPersonDao.update(thePerson); + if (thePerson.getIdElement().hasIdPart()) { + return myPersonDao.update(thePerson); + } else { + return myPersonDao.create(thePerson); + } } public IAnyResource readPersonByPid(ResourcePersistentId thePersonPid) { diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java index d2f4332078f..d3795ae2596 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/BaseMigrator.java @@ -35,6 +35,7 @@ public abstract class BaseMigrator implements IMigrator { private boolean myDryRun; private boolean myNoColumnShrink; private boolean myOutOfOrderPermitted; + private boolean mySchemaWasInitialized; private DriverTypeEnum myDriverType; private DataSource myDataSource; private List myExecutedStatements = new ArrayList<>(); @@ -111,4 +112,13 @@ public abstract class BaseMigrator implements IMigrator { } return statementBuilder; } + + public boolean isSchemaWasInitialized() { + return mySchemaWasInitialized; + } + + public BaseMigrator setSchemaWasInitialized(boolean theSchemaWasInitialized) { + mySchemaWasInitialized = theSchemaWasInitialized; + return this; + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigration.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrationTask.java similarity index 74% rename from hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigration.java rename to hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrationTask.java index f143de54a64..28b439180e0 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigration.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrationTask.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.migrate; */ import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; +import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.flywaydb.core.api.MigrationVersion; import org.flywaydb.core.api.migration.Context; @@ -32,14 +33,14 @@ import java.sql.SQLException; import static org.apache.commons.lang3.StringUtils.isBlank; -public class FlywayMigration implements JavaMigration { - private static final Logger ourLog = LoggerFactory.getLogger(FlywayMigration.class); +public class FlywayMigrationTask implements JavaMigration { + private static final Logger ourLog = LoggerFactory.getLogger(FlywayMigrationTask.class); private final BaseTask myTask; private final FlywayMigrator myFlywayMigrator; private DriverTypeEnum.ConnectionProperties myConnectionProperties; - public FlywayMigration(BaseTask theTask, FlywayMigrator theFlywayMigrator) { + public FlywayMigrationTask(BaseTask theTask, FlywayMigrator theFlywayMigrator) { myTask = theTask; myFlywayMigrator = theFlywayMigrator; } @@ -76,8 +77,7 @@ public class FlywayMigration implements JavaMigration { myTask.setNoColumnShrink(myFlywayMigrator.isNoColumnShrink()); myTask.setConnectionProperties(myConnectionProperties); try { - myTask.execute(); - myFlywayMigrator.addExecutedStatements(myTask.getExecutedStatements()); + executeTask(); } catch (SQLException e) { String description = myTask.getDescription(); if (isBlank(description)) { @@ -88,6 +88,20 @@ public class FlywayMigration implements JavaMigration { } } + private void executeTask() throws SQLException { + if (myFlywayMigrator.isSchemaWasInitialized() && !(myTask instanceof InitializeSchemaTask)) { + // Empty schema was initialized, stub out this non-schema-init task since we're starting with a fully migrated schema + myTask.setDoNothing(true); + } + myTask.execute(); + if (myTask.initializedSchema()) { + ourLog.info("Empty schema was Initialized. Stubbing out all following migration tasks that are not Schema Initializations."); + myFlywayMigrator.setSchemaWasInitialized(true); + } + + myFlywayMigrator.addExecutedStatements(myTask.getExecutedStatements()); + } + public void setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) { myConnectionProperties = theConnectionProperties; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java index c71c5d9b43d..f423667f783 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/FlywayMigrator.java @@ -40,7 +40,7 @@ public class FlywayMigrator extends BaseMigrator { private static final Logger ourLog = LoggerFactory.getLogger(FlywayMigrator.class); private final String myMigrationTableName; - private List myTasks = new ArrayList<>(); + private List myTasks = new ArrayList<>(); public FlywayMigrator(String theMigrationTableName, DataSource theDataSource, DriverTypeEnum theDriverType) { this(theMigrationTableName); @@ -53,7 +53,7 @@ public class FlywayMigrator extends BaseMigrator { } public void addTask(BaseTask theTask) { - myTasks.add(new FlywayMigration(theTask, this)); + myTasks.add(new FlywayMigrationTask(theTask, this)); } @Override @@ -80,7 +80,7 @@ public class FlywayMigrator extends BaseMigrator { .javaMigrations(myTasks.toArray(new JavaMigration[0])) .callbacks(getCallbacks().toArray(new Callback[0])) .load(); - for (FlywayMigration task : myTasks) { + for (FlywayMigrationTask task : myTasks) { task.setConnectionProperties(theConnectionProperties); } return flyway; diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java index 39b666a8e61..e47787244a8 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java @@ -230,6 +230,10 @@ public abstract class BaseTask { protected abstract void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject); + public boolean initializedSchema() { + return false; + } + public static class ExecutedStatement { private final String mySql; private final List myArguments; diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java index 3664c4c271d..f58a62fadbc 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTask.java @@ -37,6 +37,7 @@ public class InitializeSchemaTask extends BaseTask { public static final String DESCRIPTION_PREFIX = "Initialize schema for "; private final ISchemaInitializationProvider mySchemaInitializationProvider; + private boolean myInitializedSchema; public InitializeSchemaTask(String theProductVersion, String theSchemaVersion, ISchemaInitializationProvider theSchemaInitializationProvider) { super(theProductVersion, theSchemaVersion); @@ -68,9 +69,18 @@ public class InitializeSchemaTask extends BaseTask { executeSql(null, nextSql); } + if (mySchemaInitializationProvider.canInitializeSchema()) { + myInitializedSchema = true; + } + logInfo(ourLog, "{} schema for {} initialized successfully", driverType, mySchemaInitializationProvider.getSchemaDescription()); } + @Override + public boolean initializedSchema() { + return myInitializedSchema; + } + @Override protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) { InitializeSchemaTask otherObject = (InitializeSchemaTask) theOtherObject; 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 e3dc210c2c5..c85b10ba6c0 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 @@ -1144,7 +1144,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { protected void init330() { // 20180114 - 20180329 Builder version = forVersion(VersionEnum.V3_3_0); - version.initializeSchema("20180115.0", new SchemaInitializationProvider("HAPI FHIR", "/ca/uhn/hapi/fhir/jpa/docs/database", "HFJ_RESOURCE")); + version.initializeSchema("20180115.0", new SchemaInitializationProvider("HAPI FHIR", "/ca/uhn/hapi/fhir/jpa/docs/database", "HFJ_RESOURCE", true)); Builder.BuilderWithTableName hfjResource = version.onTable("HFJ_RESOURCE"); version.startSectionWithMessage("Starting work on table: " + hfjResource.getTableName()); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/SchemaInitializationProvider.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/SchemaInitializationProvider.java index 262dbb576af..d0cfbd7e73e 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/SchemaInitializationProvider.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/SchemaInitializationProvider.java @@ -41,15 +41,18 @@ public class SchemaInitializationProvider implements ISchemaInitializationProvid private String mySchemaDescription; private final String mySchemaExistsIndicatorTable; + private final boolean myCanInitializeSchema; /** * @param theSchemaFileClassPath pathname to script used to initialize schema * @param theSchemaExistsIndicatorTable a table name we can use to determine if this schema has already been initialized + * @param theCanInitializeSchema this is a "root" schema initializer that creates the primary tables used by this app */ - public SchemaInitializationProvider(String theSchemaDescription, String theSchemaFileClassPath, String theSchemaExistsIndicatorTable) { + public SchemaInitializationProvider(String theSchemaDescription, String theSchemaFileClassPath, String theSchemaExistsIndicatorTable, boolean theCanInitializeSchema) { mySchemaDescription = theSchemaDescription; mySchemaFileClassPath = theSchemaFileClassPath; mySchemaExistsIndicatorTable = theSchemaExistsIndicatorTable; + myCanInitializeSchema = theCanInitializeSchema; } @Override @@ -129,5 +132,10 @@ public class SchemaInitializationProvider implements ISchemaInitializationProvid mySchemaDescription = theSchemaDescription; return this; } + + @Override + public boolean canInitializeSchema() { + return myCanInitializeSchema; + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/ISchemaInitializationProvider.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/ISchemaInitializationProvider.java index 1be30796e25..dc7ef6b6c02 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/ISchemaInitializationProvider.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/ISchemaInitializationProvider.java @@ -30,7 +30,9 @@ public interface ISchemaInitializationProvider { String getSchemaExistsIndicatorTable(); - String getSchemaDescription(); + String getSchemaDescription(); ISchemaInitializationProvider setSchemaDescription(String theSchemaDescription); + + boolean canInitializeSchema(); } diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/FlywayMigrationTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/FlywayMigrationTaskTest.java new file mode 100644 index 00000000000..1cedd099139 --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/FlywayMigrationTaskTest.java @@ -0,0 +1,120 @@ +package ca.uhn.fhir.jpa.migrate; + +import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; +import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask; +import ca.uhn.fhir.jpa.migrate.tasks.api.ISchemaInitializationProvider; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.flywaydb.core.api.migration.Context; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FlywayMigrationTaskTest { + @Mock + private FlywayMigrator myFlywayMigrator; + @Mock + private Context myContext; + + TestTask myTestTask = new TestTask(); + + @Test + public void schemaInitializedStubsFollowingMigration() { + when(myFlywayMigrator.isSchemaWasInitialized()).thenReturn(true); + FlywayMigrationTask task = new FlywayMigrationTask(myTestTask, myFlywayMigrator); + task.migrate(myContext); + assertTrue(myTestTask.isDoNothing()); + } + + @Test + public void schemaNotInitializedStubsFollowingMigration() { + when(myFlywayMigrator.isSchemaWasInitialized()).thenReturn(false); + FlywayMigrationTask task = new FlywayMigrationTask(myTestTask, myFlywayMigrator); + task.migrate(myContext); + assertFalse(myTestTask.isDoNothing()); + } + + @Test + public void schemaInitializedStubsFollowingMigrationExceptInitSchemaTask() { + when(myFlywayMigrator.isSchemaWasInitialized()).thenReturn(true); + InitializeSchemaTask initSchemaTask = new TestInitializeSchemaTask(false); + FlywayMigrationTask task = new FlywayMigrationTask(initSchemaTask, myFlywayMigrator); + task.migrate(myContext); + assertFalse(myTestTask.isDoNothing()); + } + + @Test + public void schemaInitializedSetsInitializedFlag() { + InitializeSchemaTask initSchemaTask = new TestInitializeSchemaTask(true); + FlywayMigrationTask task = new FlywayMigrationTask(initSchemaTask, myFlywayMigrator); + task.migrate(myContext); + verify(myFlywayMigrator, times(1)).setSchemaWasInitialized(true); + } + + + @Test + public void nonInitSchemaInitializedSetsInitializedFlag() { + InitializeSchemaTask initSchemaTask = new TestInitializeSchemaTask(false); + FlywayMigrationTask task = new FlywayMigrationTask(initSchemaTask, myFlywayMigrator); + task.migrate(myContext); + verify(myFlywayMigrator, never()).setSchemaWasInitialized(true); + } + + // Can't use @Mock since BaseTask.equals is final + private class TestTask extends BaseTask { + protected TestTask() { + super("1", "1"); + } + + @Override + public void validate() { + // do nothing + } + + @Override + protected void doExecute() throws SQLException { + // do nothing + } + + @Override + protected void generateHashCode(HashCodeBuilder theBuilder) { + // do nothing + } + + @Override + protected void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject) { + // do nothing + } + } + + private class TestInitializeSchemaTask extends InitializeSchemaTask { + private final boolean myInitializedSchema; + + public TestInitializeSchemaTask(boolean theInitializedSchema) { + super("1", "1", mock(ISchemaInitializationProvider.class)); + myInitializedSchema = theInitializedSchema; + } + + @Override + public void execute() throws SQLException { + // nothing + } + + @Override + public boolean initializedSchema() { + return myInitializedSchema; + } + } +} diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTaskTest.java index bdf2a8c2b5e..7a469c372b5 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTaskTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/InitializeSchemaTaskTest.java @@ -56,6 +56,11 @@ public class InitializeSchemaTaskTest extends BaseTest { return this; } + @Override + public boolean canInitializeSchema() { + return false; + } + @Override public boolean equals(Object theO) { if (this == theO) return true; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java index 21d56329b46..5ef80a2608f 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java @@ -49,7 +49,7 @@ import java.util.ArrayList; import java.util.Collection; @Entity -@Table(name = "HFJ_RES_VER", uniqueConstraints = { +@Table(name = ResourceHistoryTable.HFJ_RES_VER, uniqueConstraints = { @UniqueConstraint(name = ResourceHistoryTable.IDX_RESVER_ID_VER, columnNames = {"RES_ID", "RES_VER"}) }, indexes = { @Index(name = "IDX_RESVER_TYPE_DATE", columnList = "RES_TYPE,RES_UPDATED"), @@ -66,6 +66,7 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl // Don't reduce the visibility here, we reference this from Smile @SuppressWarnings("WeakerAccess") public static final int ENCODING_COL_LENGTH = 5; + public static final String HFJ_RES_VER = "HFJ_RES_VER"; private static final long serialVersionUID = 1L; @Id diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index 70a13737b25..a671b26448e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -573,8 +573,11 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas @Override public String toString() { ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); - b.append("resourceType", myResourceType); b.append("pid", myId); + b.append("resourceType", myResourceType); + if (getDeleted() != null) { + b.append("deleted"); + } return b.build(); } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java index 4cb5ed5e0f3..b959fa8a210 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java @@ -94,7 +94,7 @@ public class SubscriptionRegistry { mySubscriptionChannelRegistry.add(activeSubscription); myActiveSubscriptionCache.put(subscriptionId, activeSubscription); - ourLog.info("Registered active subscription {} - Have {} registered", subscriptionId, myActiveSubscriptionCache.size()); + ourLog.info("Registered active subscription Subscription/{} - Have {} registered", subscriptionId, myActiveSubscriptionCache.size()); // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED HookParams params = new HookParams() @@ -110,6 +110,11 @@ public class SubscriptionRegistry { if (activeSubscription != null) { mySubscriptionChannelRegistry.remove(activeSubscription); ourLog.info("Unregistered active subscription {} - Have {} registered", theSubscriptionId, myActiveSubscriptionCache.size()); + + // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED + HookParams params = new HookParams(); + myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED, params); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index c346050a0dd..d973ddfc351 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -331,7 +331,7 @@ public abstract class BaseMethodBinding { } if (graphQL != null) { - return new GraphQLMethodBinding(theMethod, theContext, theProvider); + return new GraphQLMethodBinding(theMethod, graphQL.type(), theContext, theProvider); } Class returnType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java index e0af156e46e..5379f086575 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java @@ -23,7 +23,10 @@ package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -43,11 +46,17 @@ import java.lang.reflect.Method; public class GraphQLMethodBinding extends BaseMethodBinding { private final Integer myIdParamIndex; + private final Integer myQueryUrlParamIndex; + private final Integer myQueryBodyParamIndex; + private final RequestTypeEnum myMethodRequestType; - public GraphQLMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { + public GraphQLMethodBinding(Method theMethod, RequestTypeEnum theMethodRequestType, FhirContext theContext, Object theProvider) { super(theMethod, theContext, theProvider); myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, theContext); + myQueryUrlParamIndex = ParameterUtil.findParamAnnotationIndex(theMethod, GraphQLQueryUrl.class); + myQueryBodyParamIndex = ParameterUtil.findParamAnnotationIndex(theMethod, GraphQLQueryBody.class); + myMethodRequestType = theMethodRequestType; } @Override @@ -68,13 +77,23 @@ public class GraphQLMethodBinding extends BaseMethodBinding { @Override public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { - if (Constants.OPERATION_NAME_GRAPHQL.equals(theRequest.getOperation())) { + if (Constants.OPERATION_NAME_GRAPHQL.equals(theRequest.getOperation()) && myMethodRequestType.equals(theRequest.getRequestType())) { return MethodMatchEnum.EXACT; } return MethodMatchEnum.NONE; } + private String getQueryValue(Object[] methodParams) { + switch (myMethodRequestType) { + case POST: + return (String) methodParams[myQueryBodyParamIndex]; + case GET: + return (String) methodParams[myQueryUrlParamIndex]; + } + return null; + } + @Override public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { Object[] methodParams = createMethodParams(theRequest); @@ -82,7 +101,7 @@ public class GraphQLMethodBinding extends BaseMethodBinding { methodParams[myIdParamIndex] = theRequest.getId(); } - Object response = invokeServerMethod(theServer, theRequest, methodParams); + String responseString = (String) invokeServerMethod(theServer, theRequest, methodParams); int statusCode = Constants.STATUS_HTTP_200_OK; String statusMessage = Constants.HTTP_STATUS_NAMES.get(statusCode); @@ -90,20 +109,19 @@ public class GraphQLMethodBinding extends BaseMethodBinding { String charset = Constants.CHARSET_NAME_UTF8; boolean respondGzip = theRequest.isRespondGzip(); - String responseString = (String) response; - - HttpServletRequest servletRequest=null; - HttpServletResponse servletResponse=null; + HttpServletRequest servletRequest = null; + HttpServletResponse servletResponse = null; if (theRequest instanceof ServletRequestDetails) { servletRequest = ((ServletRequestDetails) theRequest).getServletRequest(); servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); } + String graphQLQuery = getQueryValue(methodParams); // Interceptor call: SERVER_OUTGOING_GRAPHQL_RESPONSE HookParams params = new HookParams() .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(String.class, theRequest.getParameters().get(Constants.PARAM_GRAPHQL_QUERY)[0]) + .add(String.class, graphQLQuery) .add(String.class, responseString) .add(HttpServletRequest.class, servletRequest) .add(HttpServletResponse.class, servletResponse); @@ -128,7 +146,6 @@ public class GraphQLMethodBinding extends BaseMethodBinding { writer.write(responseString); writer.close(); - return null; } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryBodyParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryBodyParameter.java new file mode 100644 index 00000000000..faa4aec94de --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryBodyParameter.java @@ -0,0 +1,91 @@ +package ca.uhn.fhir.rest.server.method; + +/* + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.ConfigurationException; +import ca.uhn.fhir.parser.json.JsonLikeObject; +import ca.uhn.fhir.parser.json.JsonLikeStructure; +import ca.uhn.fhir.parser.json.JsonLikeValue; +import ca.uhn.fhir.parser.json.jackson.JacksonStructure; +import ca.uhn.fhir.rest.annotation.Count; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Method; +import java.util.Collection; + +import static ca.uhn.fhir.rest.api.Constants.CT_GRAPHQL; +import static ca.uhn.fhir.rest.api.Constants.CT_JSON; +import static ca.uhn.fhir.rest.server.method.ResourceParameter.createRequestReader; + +public class GraphQLQueryBodyParameter implements IParameter { + + private Class myType; + + @Override + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); + Reader requestReader = createRequestReader(theRequest); + + if (CT_JSON.equals(ctValue)) { + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(requestReader); + if (jsonNode != null && jsonNode.get("query") != null) { + return jsonNode.get("query").asText(); + } + } catch (IOException e) { + throw new InternalErrorException(e); + } + } + + if (CT_GRAPHQL.equals(ctValue)) { + try { + return IOUtils.toString(requestReader); + } catch (IOException e) { + throw new InternalErrorException(e); + } + } + + return null; + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + if (theOuterCollectionType != null) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" +theMethod.getDeclaringClass().getCanonicalName()+ "' is annotated with @" + Count.class.getName() + " but can not be of collection type"); + } + if (!String.class.equals(theParameterType)) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' in type '" +theMethod.getDeclaringClass().getCanonicalName()+ "' is annotated with @" + Count.class.getName() + " but type '" + theParameterType + "' is an invalid type, must be one of Integer or IntegerType"); + } + myType = theParameterType; + } + +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryUrlParameter.java similarity index 97% rename from hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryParameter.java rename to hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryUrlParameter.java index f652ec563d3..a2a1e9a097c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLQueryUrlParameter.java @@ -34,7 +34,7 @@ import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Method; import java.util.Collection; -public class GraphQLQueryParameter implements IParameter { +public class GraphQLQueryUrlParameter implements IParameter { private Class myType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 1c28e21ab81..85887299e53 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -213,8 +213,10 @@ public class MethodUtil { ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType); } else if (nextAnnotation instanceof Count) { param = new CountParameter(); - } else if (nextAnnotation instanceof GraphQLQuery) { - param = new GraphQLQueryParameter(); + } else if (nextAnnotation instanceof GraphQLQueryUrl) { + param = new GraphQLQueryUrlParameter(); + } else if (nextAnnotation instanceof GraphQLQueryBody) { + param = new GraphQLQueryBodyParameter(); } else if (nextAnnotation instanceof Sort) { param = new SortParameter(theContext); } else if (nextAnnotation instanceof TransactionParam) { diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/ctx/FhirContextDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/ctx/FhirContextDstu2Test.java index cf284a52aa9..aaf8545ddbd 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/ctx/FhirContextDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/ctx/FhirContextDstu2Test.java @@ -162,7 +162,7 @@ public class FhirContextDstu2Test { }); } // wait until all threads are ready - assertTrue(allExecutorThreadsReady.await(runnables.size() * 10, TimeUnit.MILLISECONDS), "Timeout initializing threads! Perform long lasting initializations before passing runnables to assertConcurrent"); + assertTrue(allExecutorThreadsReady.await(runnables.size() * 10L, TimeUnit.MILLISECONDS), "Timeout initializing threads! Perform long lasting initializations before passing runnables to assertConcurrent"); // start all test runners afterInitBlocker.countDown(); assertTrue(allDone.await(maxTimeoutSeconds, TimeUnit.SECONDS), message + " timeout! More than" + maxTimeoutSeconds + "seconds"); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/util/FhirTerserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/util/FhirTerserDstu2Test.java index 97b7ce38495..5dbee6f6085 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/util/FhirTerserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/util/FhirTerserDstu2Test.java @@ -119,6 +119,33 @@ public class FhirTerserDstu2Test { } + @Test + public void testCloneIntoResourceCopiesId() { + Observation obs = new Observation(); + obs.setId("http://foo/base/Observation/_history/123"); + obs.setValue(new StringDt("AAA")); + + Observation target = new Observation(); + ourCtx.newTerser().cloneInto(obs, target, false); + + assertEquals("http://foo/base/Observation/_history/123", target.getId().getValue()); + } + + + @Test + public void testCloneIntoResourceCopiesElementId() { + Observation obs = new Observation(); + StringDt string = new StringDt("AAA"); + string.setId("BBB"); + obs.setValue(string); + + Observation target = new Observation(); + ourCtx.newTerser().cloneInto(obs, target, false); + + assertEquals("BBB", ((StringDt)target.getValue()).getElementSpecificId()); + } + + /** * See #369 */ diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu3Test.java index c853df525a5..b0190dbe6d9 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu3Test.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; @@ -226,7 +226,7 @@ public class AuthorizationInterceptorDstu3Test { } @GraphQL - public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { + public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQuery) { ourHitMethod = true; return "{'foo':'bar'}"; } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java index f28110d1167..f099b1abafc 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java @@ -2,12 +2,14 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; +import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.test.utilities.JettyUtil; import ca.uhn.fhir.util.TestUtil; @@ -15,6 +17,8 @@ import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -114,6 +118,62 @@ public class GraphQLR4RawTest { } + + @Test + public void testGraphPostContentTypeJson() throws Exception { + ourNextRetVal = "{\"foo\"}"; + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$graphql"); + StringEntity entity = new StringEntity("{\"query\": \"{name{family,given}}\"}"); + httpPost.setEntity(entity); + httpPost.setHeader("Accept", "application/json"); + httpPost.setHeader("Content-type", "application/json"); + + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + + assertEquals("{\"foo\"}", responseContent); + assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); + assertEquals("Patient/123", ourLastId.getValue()); + assertEquals("{name{family,given}}", ourLastQuery); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @Test + public void testGraphPostContentTypeGraphql() throws Exception { + ourNextRetVal = "{\"foo\"}"; + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$graphql"); + StringEntity entity = new StringEntity("{name{family,given}}"); + httpPost.setEntity(entity); + httpPost.setHeader("Accept", "application/json"); + httpPost.setHeader("Content-type", "application/graphql"); + + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + + assertEquals("{\"foo\"}", responseContent); + assertThat(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue(), startsWith("application/json")); + assertEquals("Patient/123", ourLastId.getValue()); + assertEquals("{name{family,given}}", ourLastQuery); + + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @Test public void testGraphInstanceUnknownType() throws Exception { ourNextRetVal = "{\"foo\"}"; @@ -157,9 +217,16 @@ public class GraphQLR4RawTest { public static class MyGraphQLProvider { + @GraphQL(type=RequestTypeEnum.GET) + public String processGet(@IdParam IdType theId, @GraphQLQueryUrl String theQuery) { + ourMethodCount++; + ourLastId = theId; + ourLastQuery = theQuery; + return ourNextRetVal; + } - @GraphQL - public String process(@IdParam IdType theId, @GraphQLQuery String theQuery) { + @GraphQL(type=RequestTypeEnum.POST) + public String processPost(@IdParam IdType theId, @GraphQLQueryBody String theQuery) { ourMethodCount++; ourLastId = theId; ourLastQuery = theQuery; diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java index ae93ee90804..32358bd175a 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Read; @@ -858,7 +858,7 @@ public class ResponseHighlightingInterceptorTest { public static class GraphQLProvider { @GraphQL - public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { + public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQuery) { return "{\"foo\":\"bar\"}"; } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java index 893398f68da..480568284fb 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java @@ -10,7 +10,7 @@ import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; import ca.uhn.fhir.rest.annotation.GraphQL; -import ca.uhn.fhir.rest.annotation.GraphQLQuery; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; @@ -3888,7 +3888,7 @@ public class AuthorizationInterceptorR4Test { } @GraphQL - public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { + public String processGraphQlRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQuery) { ourHitMethod = true; return "{'foo':'bar'}"; } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java index 7c9b557f2b5..ccef5f934cc 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java @@ -13,6 +13,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Element; import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Extension; @@ -180,15 +181,43 @@ public class FhirTerserR4Test { assertEquals("FOO", ((StringType) exts.get(0).getValue()).getValue()); } + @Test + public void testCloneIntoExtensionWithChildExtension() { + Patient patient = new Patient(); + + Extension ext = new Extension("http://example.com", new StringType("FOO")); + patient.addExtension((Extension) new Extension().setUrl("http://foo").addExtension(ext)); + + Patient target = new Patient(); + ourCtx.newTerser().cloneInto(patient, target, false); + + List exts = target.getExtensionsByUrl("http://foo"); + assertEquals(1, exts.size()); + exts = exts.get(0).getExtensionsByUrl("http://example.com"); + assertEquals("FOO", ((StringType) exts.get(0).getValue()).getValue()); + } + + @Test + public void testCloneEnumeration() { + Patient patient = new Patient(); + patient.setGender(Enumerations.AdministrativeGender.MALE); + + Patient target = new Patient(); + ourCtx.newTerser().cloneInto(patient, target, false); + + assertEquals("http://hl7.org/fhir/administrative-gender", target.getGenderElement().getSystem()); + } @Test public void testCloneIntoPrimitive() { StringType source = new StringType("STR"); + source.setId("STRING_ID"); MarkdownType target = new MarkdownType(); ourCtx.newTerser().cloneInto(source, target, true); assertEquals("STR", target.getValueAsString()); + assertEquals("STRING_ID", target.getId()); } @@ -225,6 +254,35 @@ public class FhirTerserR4Test { assertEquals("COMMENTS", obs.getNote().get(0).getText()); } + + @Test + public void testCloneIntoResourceCopiesId() { + Observation obs = new Observation(); + obs.setId("http://foo/base/Observation/_history/123"); + obs.setValue(new StringType("AAA")); + obs.addNote().setText("COMMENTS"); + + Observation target = new Observation(); + ourCtx.newTerser().cloneInto(obs, target, false); + + assertEquals("http://foo/base/Observation/_history/123", target.getId()); + } + + + @Test + public void testCloneIntoResourceCopiesElementId() { + Observation obs = new Observation(); + StringType string = new StringType("AAA"); + string.setId("BBB"); + obs.setValue(string); + + Observation target = new Observation(); + ourCtx.newTerser().cloneInto(obs, target, false); + + assertEquals("BBB", target.getValueStringType().getId()); + } + + @Test public void testGetAllPopulatedChildElementsOfTypeDescendsIntoContained() { Patient p = new Patient(); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index 8ab1ea4e4e5..b447c39e160 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -8,8 +8,11 @@ import ca.uhn.fhir.util.ClasspathUtil; import org.apache.commons.lang3.Validate; import org.fhir.ucum.UcumEssenceService; import org.fhir.ucum.UcumException; +import org.hl7.fhir.convertors.VersionConvertor_30_40; +import org.hl7.fhir.convertors.VersionConvertor_40_50; import org.hl7.fhir.dstu2.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CodeSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,9 +124,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { if (isBlank(theValueSetUrl)) { CodeValidationResult validationResult = validateLookupCode(theValidationSupportContext, theCode, theCodeSystem); - if (validationResult != null) { - return validationResult; - } + return validationResult; } return null; @@ -147,70 +148,33 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { @Override public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { + Map map; switch (theSystem) { case UCUM_CODESYSTEM_URL: - - InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml"); - try { - UcumEssenceService svc = new UcumEssenceService(input); - String outcome = svc.analyse(theCode); - if (outcome != null) { - - LookupCodeResult retVal = new LookupCodeResult(); - retVal.setSearchedForCode(theCode); - retVal.setSearchedForSystem(theSystem); - retVal.setFound(true); - retVal.setCodeDisplay(outcome); - return retVal; - - } - } catch (UcumException e) { - ourLog.debug("Failed parse UCUM code: {}", theCode, e); - return null; - } finally { - ClasspathUtil.close(input); - } - break; - - case COUNTRIES_CODESYSTEM_URL: - - String display = ISO_3166_CODES.get(theCode); - if (isNotBlank(display)) { - LookupCodeResult retVal = new LookupCodeResult(); - retVal.setSearchedForCode(theCode); - retVal.setSearchedForSystem(theSystem); - retVal.setFound(true); - retVal.setCodeDisplay(display); - return retVal; - } - break; - + return lookupUcumCode(theCode); case MIMETYPES_CODESYSTEM_URL: - - // This is a pretty naive implementation - Should be enhanced in future - LookupCodeResult mimeRetVal = new LookupCodeResult(); - mimeRetVal.setSearchedForCode(theCode); - mimeRetVal.setSearchedForSystem(theSystem); - mimeRetVal.setFound(true); - return mimeRetVal; - - case CURRENCIES_CODESYSTEM_URL: - - String currenciesDisplay = ISO_3166_CODES.get(theCode); - if (isNotBlank(currenciesDisplay)) { - LookupCodeResult retVal = new LookupCodeResult(); - retVal.setSearchedForCode(theCode); - retVal.setSearchedForSystem(theSystem); - retVal.setFound(true); - retVal.setCodeDisplay(currenciesDisplay); - return retVal; - } + return lookupMimetypeCode(theCode); + case COUNTRIES_CODESYSTEM_URL: + map = ISO_3166_CODES; + break; + case CURRENCIES_CODESYSTEM_URL: + map = ISO_4217_CODES; + break; + case USPS_CODESYSTEM_URL: + map = USPS_CODES; break; - default: - return null; + } + String display = map.get(theCode); + if (isNotBlank(display)) { + LookupCodeResult retVal = new LookupCodeResult(); + retVal.setSearchedForCode(theCode); + retVal.setSearchedForSystem(theSystem); + retVal.setFound(true); + retVal.setCodeDisplay(display); + return retVal; } // If we get here it means we know the codesystem but the code was bad @@ -222,6 +186,82 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { } + @Nonnull + private LookupCodeResult lookupMimetypeCode(String theCode) { + // This is a pretty naive implementation - Should be enhanced in future + LookupCodeResult mimeRetVal = new LookupCodeResult(); + mimeRetVal.setSearchedForCode(theCode); + mimeRetVal.setSearchedForSystem(MIMETYPES_CODESYSTEM_URL); + mimeRetVal.setFound(true); + return mimeRetVal; + } + + @Nonnull + private LookupCodeResult lookupUcumCode(String theCode) { + InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml"); + String outcome = null; + try { + UcumEssenceService svc = new UcumEssenceService(input); + outcome = svc.analyse(theCode); + } catch (UcumException e) { + ourLog.warn("Failed parse UCUM code: {}", theCode, e); + } finally { + ClasspathUtil.close(input); + } + LookupCodeResult retVal = new LookupCodeResult(); + retVal.setSearchedForCode(theCode); + retVal.setSearchedForSystem(UCUM_CODESYSTEM_URL); + if (outcome != null) { + retVal.setFound(true); + retVal.setCodeDisplay(outcome); + } + return retVal; + } + + @Override + public IBaseResource fetchCodeSystem(String theSystem) { + + Map map; + switch (defaultString(theSystem)) { + case COUNTRIES_CODESYSTEM_URL: + map = ISO_3166_CODES; + break; + case CURRENCIES_CODESYSTEM_URL: + map = ISO_4217_CODES; + break; + default: + return null; + } + + CodeSystem retVal = new CodeSystem(); + retVal.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + retVal.setUrl(theSystem); + for (Map.Entry nextEntry : map.entrySet()) { + retVal.addConcept().setCode(nextEntry.getKey()).setDisplay(nextEntry.getValue()); + } + + IBaseResource normalized = null; + switch (getFhirContext().getVersion().getVersion()) { + case DSTU2: + case DSTU2_HL7ORG: + case DSTU2_1: + return null; + case DSTU3: + normalized = VersionConvertor_30_40.convertResource(retVal, false); + break; + case R4: + normalized = retVal; + break; + case R5: + normalized = VersionConvertor_40_50.convertResource(retVal); + break; + } + + Validate.notNull(normalized); + + return normalized; + } + @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { @@ -229,6 +269,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { case COUNTRIES_CODESYSTEM_URL: case UCUM_CODESYSTEM_URL: case MIMETYPES_CODESYSTEM_URL: + case USPS_CODESYSTEM_URL: return true; } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 35973dd41cb..3fca6d034ac 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; +import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.util.VersionIndependentConcept; import org.apache.commons.lang3.Validate; import org.hl7.fhir.convertors.conv10_50.ValueSet10_50; @@ -26,6 +27,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -90,8 +93,11 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu private org.hl7.fhir.r5.model.ValueSet expandValueSetToCanonical(ValidationSupportContext theValidationSupportContext, IBaseResource theValueSetToExpand, @Nullable String theWantSystem, @Nullable String theWantCode) { org.hl7.fhir.r5.model.ValueSet expansionR5; - switch (myCtx.getVersion().getVersion()) { - case DSTU2: + switch (theValueSetToExpand.getStructureFhirVersionEnum()) { + case DSTU2: { + expansionR5 = expandValueSetDstu2(theValidationSupportContext, (ca.uhn.fhir.model.dstu2.resource.ValueSet) theValueSetToExpand, theWantSystem, theWantCode); + break; + } case DSTU2_HL7ORG: { expansionR5 = expandValueSetDstu2Hl7Org(theValidationSupportContext, (ValueSet) theValueSetToExpand, theWantSystem, theWantCode); break; @@ -122,11 +128,11 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu @Override public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { - org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theValidationSupportContext, theValueSet, theCodeSystem, theCode); + org.hl7.fhir.r5.model.ValueSet expansion = expandValueSetToCanonical(theValidationSupportContext, theValueSet, theCodeSystem, theCode); if (expansion == null) { return null; } - return validateCodeInExpandedValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, expansion); + return validateCodeInExpandedValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, expansion); } @@ -174,11 +180,11 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu IBaseResource expansion = valueSetExpansionOutcome.getValueSet(); - return validateCodeInExpandedValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, expansion); + return validateCodeInExpandedValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, expansion); } - private CodeValidationResult validateCodeInExpandedValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, IBaseResource theExpansion) { + private CodeValidationResult validateCodeInExpandedValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, IBaseResource theExpansion) { assert theExpansion != null; boolean caseSensitive = true; @@ -269,11 +275,20 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } if (codeMatches) { if (theOptions.isInferSystem() || nextExpansionCode.getSystem().equals(theCodeSystem)) { - return new CodeValidationResult() - .setCode(theCode) - .setDisplay(nextExpansionCode.getDisplay()) - .setCodeSystemName(codeSystemName) - .setCodeSystemVersion(codeSystemVersion); + if (!theOptions.isValidateDisplay() || (isBlank(nextExpansionCode.getDisplay()) || isBlank(theDisplay) || nextExpansionCode.getDisplay().equals(theDisplay))) { + return new CodeValidationResult() + .setCode(theCode) + .setDisplay(nextExpansionCode.getDisplay()) + .setCodeSystemName(codeSystemName) + .setCodeSystemVersion(codeSystemVersion); + } else { + return new CodeValidationResult() + .setSeverity(IssueSeverity.ERROR) + .setDisplay(nextExpansionCode.getDisplay()) + .setMessage("Concept Display \"" + theDisplay + "\" does not match expected \"" + nextExpansionCode.getDisplay() + "\"") + .setCodeSystemName(codeSystemName) + .setCodeSystemVersion(codeSystemVersion); + } } } } @@ -316,6 +331,33 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu return (output); } + @Nullable + private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu2(ValidationSupportContext theValidationSupportContext, ca.uhn.fhir.model.dstu2.resource.ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { + IParser parserRi = FhirContext.forCached(FhirVersionEnum.DSTU2_HL7ORG).newJsonParser(); + IParser parserHapi = FhirContext.forCached(FhirVersionEnum.DSTU2).newJsonParser(); + + Function codeSystemLoader = t -> { +// ca.uhn.fhir.model.dstu2.resource.ValueSet codeSystem = (ca.uhn.fhir.model.dstu2.resource.ValueSet) theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(t); + ca.uhn.fhir.model.dstu2.resource.ValueSet codeSystem = theInput; + CodeSystem retVal = null; + if (codeSystem != null) { + retVal = new CodeSystem(); + retVal.setUrl(codeSystem.getUrl()); + addCodesDstu2(codeSystem.getCodeSystem().getConcept(), retVal.getConcept()); + } + return retVal; + }; + Function valueSetLoader = t -> { + ca.uhn.fhir.model.dstu2.resource.ValueSet valueSet = (ca.uhn.fhir.model.dstu2.resource.ValueSet) theValidationSupportContext.getRootValidationSupport().fetchValueSet(t); + org.hl7.fhir.dstu2.model.ValueSet valueSetRi = parserRi.parseResource(org.hl7.fhir.dstu2.model.ValueSet.class, parserHapi.encodeResourceToString(valueSet)); + return ValueSet10_50.convertValueSet(valueSetRi); + }; + + org.hl7.fhir.dstu2.model.ValueSet valueSetRi = parserRi.parseResource(org.hl7.fhir.dstu2.model.ValueSet.class, parserHapi.encodeResourceToString(theInput)); + org.hl7.fhir.r5.model.ValueSet input = ValueSet10_50.convertValueSet(valueSetRi); + org.hl7.fhir.r5.model.ValueSet output = expandValueSetR5(theValidationSupportContext, input, codeSystemLoader, valueSetLoader, theWantSystem, theWantCode); + return (output); + } @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { @@ -353,6 +395,14 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } } + private void addCodesDstu2(List theSourceList, List theTargetList) { + for (ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept nextSource : theSourceList) { + CodeSystem.ConceptDefinitionComponent targetConcept = new CodeSystem.ConceptDefinitionComponent().setCode(nextSource.getCode()).setDisplay(nextSource.getDisplay()); + theTargetList.add(targetConcept); + addCodesDstu2(nextSource.getConcept(), targetConcept.getConcept()); + } + } + @Nullable private org.hl7.fhir.r5.model.ValueSet expandValueSetDstu3(ValidationSupportContext theValidationSupportContext, org.hl7.fhir.dstu3.model.ValueSet theInput, @Nullable String theWantSystem, @Nullable String theWantCode) { Function codeSystemLoader = t -> { @@ -452,6 +502,33 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu addCodes(system, codesList, nextCodeList, wantCodes); ableToHandleCode = true; } + } else if (theComposeListIsInclude) { + + /* + * If we're doing an expansion specifically looking for a single code, that means we're validating that code. + * In the case where we have a ValueSet that explicitly enumerates a collection of codes + * (via ValueSet.compose.include.code) in a code system that is unknown we'll assume the code is valid + * even iof we can't find the CodeSystem. This is a compromise obviously, since it would be ideal for + * CodeSystems to always be known, but realistically there are always going to be CodeSystems that + * can't be supplied because of copyright issues, or because they are grammar based. Allowing a VS to + * enumerate a set of good codes for them is a nice compromise there. + */ + for (org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent next : theComposeList) { + if (Objects.equals(next.getSystem(), theWantSystem)) { + Optional matchingEnumeratedConcept = next.getConcept().stream().filter(t -> Objects.equals(t.getCode(), theWantCode)).findFirst(); + if (matchingEnumeratedConcept.isPresent()) { + CodeSystem.ConceptDefinitionComponent conceptDefinition = new CodeSystem.ConceptDefinitionComponent() + .addConcept() + .setCode(theWantCode) + .setDisplay(matchingEnumeratedConcept.get().getDisplay()); + List codesList = Collections.singletonList(conceptDefinition); + addCodes(system, codesList, nextCodeList, wantCodes); + ableToHandleCode = true; + break; + } + } + } + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java index 84beb30ef0e..094a2402bad 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java @@ -3,6 +3,7 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import org.apache.commons.lang3.Validate; @@ -18,6 +19,8 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import static org.apache.commons.lang3.StringUtils.isBlank; + /** * Simple validation support module that handles profile snapshot generation. *

@@ -75,9 +78,14 @@ public class SnapshotGeneratingValidationSupport implements IValidationSupport { } theValidationSupportContext.getCurrentlyGeneratingSnapshots().add(inputUrl); - IBaseResource base = theValidationSupportContext.getRootValidationSupport().fetchStructureDefinition(inputCanonical.getBaseDefinition()); + String baseDefinition = inputCanonical.getBaseDefinition(); + if (isBlank(baseDefinition)) { + throw new PreconditionFailedException("StructureDefinition[id=" + inputCanonical.getIdElement().getId() + ", url=" + inputCanonical.getUrl() + "] has no base"); + } + + IBaseResource base = theValidationSupportContext.getRootValidationSupport().fetchStructureDefinition(baseDefinition); if (base == null) { - throw new PreconditionFailedException("Unknown base definition: " + inputCanonical.getBaseDefinition()); + throw new PreconditionFailedException("Unknown base definition: " + baseDefinition); } org.hl7.fhir.r5.model.StructureDefinition baseCanonical = (org.hl7.fhir.r5.model.StructureDefinition) converter.toCanonical(base); @@ -112,6 +120,8 @@ public class SnapshotGeneratingValidationSupport implements IValidationSupport { return theInput; + } catch (BaseServerResponseException e) { + throw e; } catch (Exception e) { throw new InternalErrorException("Failed to generate snapshot", e); } finally { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index 0ef8b423be7..848ab9e1d5c 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -471,8 +471,9 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo @Override public ValidationResult validateCode(ValidationOptions theOptions, String system, String code, String display) { - IValidationSupport.CodeValidationResult result = myValidationSupportContext.getRootValidationSupport().validateCode(myValidationSupportContext, convertConceptValidationOptions(theOptions), system, code, display, null); - return convertValidationResult(result); + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions); + + return doValidation(null, validationOptions, system, code, display); } @Override @@ -487,8 +488,9 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo throw new InternalErrorException(e); } - IValidationSupport.CodeValidationResult result = myValidationSupportContext.getRootValidationSupport().validateCodeInValueSet(myValidationSupportContext, convertConceptValidationOptions(theOptions), theSystem, theCode, display, convertedVs); - return convertValidationResult(result); + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions); + + return doValidation(convertedVs, validationOptions, theSystem, theCode, display); } @Override @@ -502,12 +504,13 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo throw new InternalErrorException(e); } - IValidationSupport.CodeValidationResult result = myValidationSupportContext.getRootValidationSupport().validateCodeInValueSet(myValidationSupportContext, convertConceptValidationOptions(theOptions).setInferSystem(true), null, code, null, convertedVs); - return convertValidationResult(result); + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions).setInferSystem(true); + + return doValidation(convertedVs, validationOptions, null, code, null); } @Override - public ValidationResult validateCode(ValidationOptions theOptions, org.hl7.fhir.r5.model.Coding code, org.hl7.fhir.r5.model.ValueSet theValueSet) { + public ValidationResult validateCode(ValidationOptions theOptions, org.hl7.fhir.r5.model.Coding theCoding, org.hl7.fhir.r5.model.ValueSet theValueSet) { IBaseResource convertedVs = null; try { @@ -518,7 +521,22 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo throw new InternalErrorException(e); } - IValidationSupport.CodeValidationResult result = myValidationSupportContext.getRootValidationSupport().validateCodeInValueSet(myValidationSupportContext, convertConceptValidationOptions(theOptions), code.getSystem(), code.getCode(), code.getDisplay(), convertedVs); + ConceptValidationOptions validationOptions = convertConceptValidationOptions(theOptions); + String system = theCoding.getSystem(); + String code = theCoding.getCode(); + String display = theCoding.getDisplay(); + + return doValidation(convertedVs, validationOptions, system, code, display); + } + + @Nonnull + private ValidationResult doValidation(IBaseResource theValueSet, ConceptValidationOptions theValidationOptions, String theSystem, String theCode, String theDisplay) { + IValidationSupport.CodeValidationResult result; + if (theValueSet != null) { + result = myValidationSupportContext.getRootValidationSupport().validateCodeInValueSet(myValidationSupportContext, theValidationOptions, theSystem, theCode, theDisplay, theValueSet); + } else { + result = myValidationSupportContext.getRootValidationSupport().validateCode(myValidationSupportContext, theValidationOptions, theSystem, theCode, theDisplay, null); + } return convertValidationResult(result); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java index 1a8e0c06c9d..1be573dd025 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java @@ -1,9 +1,12 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,13 +40,13 @@ public class CommonCodeSystemsTerminologyServiceTest { @Test public void testUcum_LookupCode_Bad() { IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(new ValidationSupportContext(myCtx.getValidationSupport()), "http://unitsofmeasure.org", "AAAAA"); - assertNull( outcome); + assertEquals(false, outcome.isFound()); } @Test public void testUcum_LookupCode_UnknownSystem() { IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(new ValidationSupportContext(myCtx.getValidationSupport()), "http://foo", "AAAAA"); - assertNull( outcome); + assertNull(outcome); } @Test @@ -72,4 +75,43 @@ public class CommonCodeSystemsTerminologyServiceTest { assertNull(outcome); } + @Test + public void testFetchCodeSystemBuiltIn_Iso3166_R4() { + CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); + assertEquals(498, cs.getConcept().size()); + } + + @Test + public void testFetchCodeSystemBuiltIn_Iso3166_DSTU3() { + CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forCached(FhirVersionEnum.DSTU3)); + org.hl7.fhir.dstu3.model.CodeSystem cs = (org.hl7.fhir.dstu3.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); + assertEquals(498, cs.getConcept().size()); + } + + @Test + public void testFetchCodeSystemBuiltIn_Iso3166_R5() { + CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forCached(FhirVersionEnum.R5)); + org.hl7.fhir.r5.model.CodeSystem cs = (org.hl7.fhir.r5.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); + assertEquals(498, cs.getConcept().size()); + } + + @Test + public void testFetchCodeSystemBuiltIn_Iso3166_DSTU2() { + CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forCached(FhirVersionEnum.DSTU2)); + IBaseResource cs = svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); + assertEquals(null, cs); + } + + @Test + public void testFetchCodeSystemBuiltIn_Iso_R4() { + CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL); + assertEquals(182, cs.getConcept().size()); + } + + @Test + public void testFetchCodeSystemBuiltIn_Unknown() { + CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem("http://foo"); + assertEquals(null, cs); + } + } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java new file mode 100644 index 00000000000..96136c55255 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupportTest.java @@ -0,0 +1,58 @@ +package org.hl7.fhir.common.hapi.validation.support; + +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 org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class InMemoryTerminologyServerValidationSupportTest { + + private InMemoryTerminologyServerValidationSupport mySvc; + private FhirContext myCtx = FhirContext.forR4(); + private DefaultProfileValidationSupport myDefaultSupport; + private ValidationSupportChain myChain; + private PrePopulatedValidationSupport myPrePopulated; + + @BeforeEach + public void before( ){ + mySvc = new InMemoryTerminologyServerValidationSupport(myCtx); + myDefaultSupport = new DefaultProfileValidationSupport(myCtx); + myPrePopulated = new PrePopulatedValidationSupport(myCtx); + myChain = new ValidationSupportChain(mySvc,myPrePopulated, myDefaultSupport); + + // Force load + myDefaultSupport.fetchCodeSystem("http://foo"); + } + + @Test + public void testValidateCodeInUnknownCodeSystemWithEnumeratedValueSet() { + ValueSet vs = new ValueSet(); + vs.setUrl("http://vs"); + vs + .getCompose() + .addInclude() + .setSystem("http://cs") + .addConcept(new ValueSet.ConceptReferenceComponent(new CodeType("code1"))) + .addConcept(new ValueSet.ConceptReferenceComponent(new CodeType("code2"))); + myPrePopulated.addValueSet(vs); + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + ConceptValidationOptions options = new ConceptValidationOptions(); + + IValidationSupport.CodeValidationResult outcome = myChain.validateCodeInValueSet(valCtx, options, "http://cs", "code1", null, vs); + assertTrue(outcome.isOk()); + + outcome = myChain.validateCodeInValueSet(valCtx, options, "http://cs", "code99", null, vs); + assertNull(outcome); + + } + + +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index f906a4aa8ed..26524f3f3dd 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -352,7 +352,7 @@ public class FhirInstanceValidatorDstu3Test { List all = logResultsAndReturnAll(result); assertEquals(1, all.size()); assertEquals(ResultSeverityEnum.ERROR, all.get(0).getSeverity()); - assertEquals("Validation failed for \"urn:iso:std:iso:3166#QQ\"", all.get(0).getMessage()); + assertEquals("Unknown code 'urn:iso:std:iso:3166#QQ' for \"urn:iso:std:iso:3166#QQ\"", all.get(0).getMessage()); } } diff --git a/pom.xml b/pom.xml index 9a62f4f2fc5..436efb05347 100644 --- a/pom.xml +++ b/pom.xml @@ -906,6 +906,11 @@ commons-csv 1.7 + + org.aspectj + aspectjweaver + 1.9.5 + org.hl7.fhir.testcases fhir-test-cases