diff --git a/.editorconfig b/.editorconfig index 31b0bd4a6a7..c42d0f67472 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,11 @@ tab_width = 4 indent_size = 4 charset = utf-8 +[*.html] +indent_style = tab +tab_width = 3 +indent_size = 3 + [*.xml] indent_style = tab tab_width = 3 diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 629a8c9e5e9..fb46b3a9ec3 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 43446cdd944..04fca17ca7b 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 8ff07e5a1db..7e0e0d1fd79 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IModelJson.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IModelJson.java index 0157b5216fd..3bdc16a5f85 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IModelJson.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IModelJson.java @@ -29,4 +29,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) -public interface IModelJson {} +public interface IModelJson { + String SENSITIVE_DATA_FILTER_NAME = "sensitiveDataFilter"; +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/SensitiveNoDisplay.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/SensitiveNoDisplay.java new file mode 100644 index 00000000000..58978623926 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/SensitiveNoDisplay.java @@ -0,0 +1,34 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.model.api.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a field as sensitive, indicating that it should not + * be displayed or serialized by jackson. The only way to serialize an object annotated with this annotation is to use + * {@link ca.uhn.fhir.util.JsonUtil}, as it has a registered filter against this annotation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SensitiveNoDisplay {} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/valueset/BundleTypeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/valueset/BundleTypeEnum.java index 384356337b0..0710a5533f4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/valueset/BundleTypeEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/valueset/BundleTypeEnum.java @@ -91,7 +91,7 @@ public enum BundleTypeEnum { /** * Returns the enumerated value associated with this code */ - public BundleTypeEnum forCode(String theCode) { + public static BundleTypeEnum forCode(String theCode) { BundleTypeEnum retVal = CODE_TO_ENUM.get(theCode); return retVal; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java index a2b8919e50a..ea192901aa2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.narrative2.BaseNarrativeGenerator; import ca.uhn.fhir.narrative2.INarrativeTemplate; +import ca.uhn.fhir.narrative2.NarrativeGeneratorTemplateUtils; import ca.uhn.fhir.narrative2.TemplateTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.collect.Sets; @@ -109,6 +110,7 @@ public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGener Context context = new Context(); context.setVariable("resource", theTargetContext); context.setVariable("context", theTargetContext); + context.setVariable("narrativeUtil", NarrativeGeneratorTemplateUtils.INSTANCE); context.setVariable( "fhirVersion", theFhirContext.getVersion().getVersion().name()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java new file mode 100644 index 00000000000..21cdab2dcde --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java @@ -0,0 +1,53 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.narrative2; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.BundleUtil; +import org.apache.commons.lang3.tuple.Pair; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; +import java.util.Objects; + +/** + * An instance of this class is added to the Thymeleaf context as a variable with + * name "narrativeUtil" and can be accessed from narrative templates. + * + * @since 7.0.0 + */ +public class NarrativeGeneratorTemplateUtils { + + public static final NarrativeGeneratorTemplateUtils INSTANCE = new NarrativeGeneratorTemplateUtils(); + + /** + * Given a Bundle as input, are any entries present with a given resource type + */ + public boolean bundleHasEntriesWithResourceType(IBaseBundle theBaseBundle, String theResourceType) { + FhirContext ctx = theBaseBundle.getStructureFhirVersionEnum().newContextCached(); + List> entryResources = + BundleUtil.getBundleEntryUrlsAndResources(ctx, theBaseBundle); + return entryResources.stream() + .map(Pair::getValue) + .filter(Objects::nonNull) + .anyMatch(t -> ctx.getResourceType(t).equals(theResourceType)); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java index 62983e9e5e7..8e07ade6766 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java @@ -1413,14 +1413,18 @@ class ParserState { myErrorHandler.invalidValue(location, value, "Attribute value must not be empty (\"\")"); } else { - /* - * It may be possible to clean this up somewhat once the following PR is hopefully merged: - * https://github.com/FasterXML/jackson-core/pull/611 - * - * See TolerantJsonParser - */ if ("decimal".equals(myTypeName)) { - if (value != null) + if (value != null) { + // remove leading plus sign from decimal value + if (value.startsWith("+")) { + value = value.substring(1); + } + /* + * It may be possible to clean this up somewhat once the following PR is hopefully merged: + * https://github.com/FasterXML/jackson-core/pull/611 + * + * See TolerantJsonParser + */ if (value.startsWith(".") && NumberUtils.isDigits(value.substring(1))) { value = "0" + value; } else { @@ -1428,6 +1432,7 @@ class ParserState { value = value.substring(1); } } + } } try { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/json/jackson/JacksonStructure.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/json/jackson/JacksonStructure.java index 160a976bea3..708223cf917 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/json/jackson/JacksonStructure.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/json/jackson/JacksonStructure.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -406,7 +407,9 @@ public class JacksonStructure implements JsonLikeStructure { } private static ObjectMapper createObjectMapper() { - ObjectMapper retVal = JsonMapper.builder().build(); + ObjectMapper retVal = JsonMapper.builder() + .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS) + .build(); retVal = retVal.setNodeFactory(new JsonNodeFactory(true)); retVal = retVal.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); retVal = retVal.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); 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 5d43ea2052d..0c962c63340 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 @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -232,6 +233,8 @@ public class Constants { public static final String PARAMQUALIFIER_NICKNAME = ":nickname"; public static final String PARAMQUALIFIER_TOKEN_OF_TYPE = ":of-type"; public static final String PARAMQUALIFIER_TOKEN_NOT = ":not"; + public static final String PARAMQUALIFIER_TOKEN_IDENTIFIER = ":identifier"; + public static final int STATUS_HTTP_200_OK = 200; public static final int STATUS_HTTP_201_CREATED = 201; public static final int STATUS_HTTP_204_NO_CONTENT = 204; @@ -314,6 +317,17 @@ public class Constants { public static final String PARAMQUALIFIER_TOKEN_NOT_IN = ":not-in"; public static final String PARAMQUALIFIER_TOKEN_ABOVE = ":above"; public static final String PARAMQUALIFIER_TOKEN_BELOW = ":below"; + + public static final List VALID_MODIFIERS = Collections.unmodifiableList(Arrays.asList( + PARAMQUALIFIER_STRING_CONTAINS, + PARAMQUALIFIER_STRING_EXACT, + PARAMQUALIFIER_TOKEN_IN, + PARAM_INCLUDE_QUALIFIER_ITERATE, + PARAMQUALIFIER_MISSING, + PARAMQUALIFIER_TOKEN_NOT_IN, + PARAMQUALIFIER_TOKEN_OF_TYPE, + PARAM_INCLUDE_QUALIFIER_RECURSE, + PARAMQUALIFIER_TOKEN_TEXT)); /** * The number of characters in a UUID (36) */ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java index 783d71682f5..cee970715af 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/AttachmentUtil.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.primitive.CodeDt; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.ICompositeType; @@ -41,8 +42,8 @@ public class AttachmentUtil { return getOrCreateChild(theContext, theAttachment, "data", "base64Binary"); } - public static IPrimitiveType getOrCreateContentType(FhirContext theContext, ICompositeType theAttachment) { - return getOrCreateChild(theContext, theAttachment, "contentType", "string"); + public static IPrimitiveType getOrCreateContentType(FhirContext theContext, ICompositeType theAttachment) { + return getOrCreateChild(theContext, theAttachment, "contentType", "code"); } public static IPrimitiveType getOrCreateUrl(FhirContext theContext, ICompositeType theAttachment) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index 842fd855508..2b030a8953e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -251,10 +251,22 @@ public class BundleBuilder { * @param theResource The resource to create */ public CreateBuilder addTransactionCreateEntry(IBaseResource theResource) { + return addTransactionCreateEntry(theResource, null); + } + + /** + * Adds an entry containing an create (POST) request. + * Also sets the Bundle.type value to "transaction" if it is not already set. + * + * @param theResource The resource to create + * @param theFullUrl The fullUrl to attach to the entry. If null, will default to the resource ID. + */ + public CreateBuilder addTransactionCreateEntry(IBaseResource theResource, @Nullable String theFullUrl) { setBundleField("type", "transaction"); - IBase request = - addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); + IBase request = addEntryAndReturnRequest( + theResource, + theFullUrl != null ? theFullUrl : theResource.getIdElement().getValue()); String resourceType = myContext.getResourceType(theResource); @@ -423,7 +435,7 @@ public class BundleBuilder { */ public void addCollectionEntry(IBaseResource theResource) { setType("collection"); - addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); + addEntryAndReturnRequest(theResource); } /** @@ -431,7 +443,7 @@ public class BundleBuilder { */ public void addDocumentEntry(IBaseResource theResource) { setType("document"); - addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); + addEntryAndReturnRequest(theResource); } /** @@ -463,6 +475,14 @@ public class BundleBuilder { return (IBaseBackboneElement) searchInstance; } + private IBase addEntryAndReturnRequest(IBaseResource theResource) { + IIdType id = theResource.getIdElement(); + if (id.hasVersionIdPart()) { + id = id.toVersionless(); + } + return addEntryAndReturnRequest(theResource, id.getValue()); + } + private IBase addEntryAndReturnRequest(IBaseResource theResource, String theFullUrl) { Validate.notNull(theResource, "theResource must not be null"); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java index c31a2807136..3bd503eb5c5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -235,6 +236,14 @@ public class BundleUtil { return null; } + public static BundleTypeEnum getBundleTypeEnum(FhirContext theContext, IBaseBundle theBundle) { + String bundleTypeCode = BundleUtil.getBundleType(theContext, theBundle); + if (isBlank(bundleTypeCode)) { + return null; + } + return BundleTypeEnum.forCode(bundleTypeCode); + } + public static void setBundleType(FhirContext theContext, IBaseBundle theBundle, String theType) { RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); BaseRuntimeChildDefinition entryChild = def.getChildByName("type"); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/JsonUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/JsonUtil.java index 41313311c33..9a56f93a1e4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/JsonUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/JsonUtil.java @@ -21,15 +21,23 @@ package ca.uhn.fhir.util; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.api.IModelJson; +import ca.uhn.fhir.model.api.annotation.SensitiveNoDisplay; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.databind.ser.PropertyWriter; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import jakarta.annotation.Nonnull; import java.io.IOException; +import java.io.InputStream; import java.io.StringWriter; import java.io.Writer; import java.util.List; @@ -38,15 +46,30 @@ public class JsonUtil { private static final ObjectMapper ourMapperPrettyPrint; private static final ObjectMapper ourMapperNonPrettyPrint; + private static final ObjectMapper ourMapperIncludeSensitive; + + public static final SimpleBeanPropertyFilter SIMPLE_BEAN_PROPERTY_FILTER = new SensitiveDataFilter(); + + public static final SimpleFilterProvider SENSITIVE_DATA_FILTER_PROVIDER = + new SimpleFilterProvider().addFilter(IModelJson.SENSITIVE_DATA_FILTER_NAME, SIMPLE_BEAN_PROPERTY_FILTER); + public static final SimpleFilterProvider SHOW_ALL_DATA_FILTER_PROVIDER = new SimpleFilterProvider() + .addFilter(IModelJson.SENSITIVE_DATA_FILTER_NAME, SimpleBeanPropertyFilter.serializeAll()); static { ourMapperPrettyPrint = new ObjectMapper(); ourMapperPrettyPrint.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ourMapperPrettyPrint.setFilterProvider(SENSITIVE_DATA_FILTER_PROVIDER); ourMapperPrettyPrint.enable(SerializationFeature.INDENT_OUTPUT); ourMapperNonPrettyPrint = new ObjectMapper(); ourMapperNonPrettyPrint.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ourMapperNonPrettyPrint.setFilterProvider(SENSITIVE_DATA_FILTER_PROVIDER); ourMapperNonPrettyPrint.disable(SerializationFeature.INDENT_OUTPUT); + + ourMapperIncludeSensitive = new ObjectMapper(); + ourMapperIncludeSensitive.setFilterProvider(SHOW_ALL_DATA_FILTER_PROVIDER); + ourMapperIncludeSensitive.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ourMapperIncludeSensitive.disable(SerializationFeature.INDENT_OUTPUT); } /** @@ -67,6 +90,24 @@ public class JsonUtil { public static List deserializeList(@Nonnull String theInput, @Nonnull Class theType) throws IOException { return ourMapperPrettyPrint.readerForListOf(theType).readValue(theInput); } + /** + * Parse JSON + */ + public static T deserialize(@Nonnull InputStream theInput, @Nonnull Class theType) throws IOException { + return ourMapperPrettyPrint.readerFor(theType).readValue(theInput); + } + + /** + * Includes fields which are annotated with {@link SensitiveNoDisplay}. Currently only meant to be used for serialization + * for batch job parameters. + */ + public static String serializeWithSensitiveData(@Nonnull IModelJson theInput) { + try { + return ourMapperIncludeSensitive.writeValueAsString(theInput); + } catch (JsonProcessingException e) { + throw new InvalidRequestException(Msg.code(2487) + "Failed to encode " + theInput.getClass(), e); + } + } /** * Encode JSON @@ -93,6 +134,10 @@ public class JsonUtil { } } + public FilterProvider getSensitiveDataFilterProvider() { + return SENSITIVE_DATA_FILTER_PROVIDER; + } + /** * Encode JSON */ @@ -111,4 +156,26 @@ public class JsonUtil { throw new InvalidRequestException(Msg.code(1741) + "Failed to encode " + theJson.getClass(), e); } } + + private static class SensitiveDataFilter extends SimpleBeanPropertyFilter { + + @Override + protected boolean include(PropertyWriter writer) { + return true; // Default include all except explicitly checked and excluded + } + + @Override + public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider provider, PropertyWriter writer) + throws Exception { + if (include(writer)) { + if (!isFieldSensitive(writer)) { + super.serializeAsField(pojo, gen, provider, writer); + } + } + } + + private boolean isFieldSensitive(PropertyWriter writer) { + return writer.getAnnotation(SensitiveNoDisplay.class) != null; + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java index 192ca9e3c90..672943539f3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.util; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -82,6 +83,12 @@ public class ValidateUtil { } } + public static void isTrueOrThrowResourceNotFound(boolean theSuccess, String theMessage, Object... theValues) { + if (!theSuccess) { + throw new ResourceNotFoundException(Msg.code(2494) + String.format(theMessage, theValues)); + } + } + public static void exactlyOneNotNullOrThrowInvalidRequestException(Object[] theObjects, String theMessage) { int count = 0; for (Object next : theObjects) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index c97e1c4935f..d59f373bee3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -135,6 +135,8 @@ public enum VersionEnum { V6_11_0, V7_0_0, + V7_0_1, + V7_1_0, V7_2_0; diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index da21fd50e4f..fae7bf1c491 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -89,6 +89,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid m ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search ca.uhn.fhir.jpa.dao.BaseStorageDao.inlineMatchNotSupported=Inline match URLs are not supported on this server. Cannot process reference: "{0}" ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithMultipleMatchFailure=Failed to {0} resource with match URL "{1}" because this search matched {2} resources +ca.uhn.fhir.jpa.dao.BaseStorageDao.deleteByUrlThresholdExceeded=Failed to DELETE resources with match URL "{0}" because the resolved number of resources: {1} exceeds the threshold of {2} ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithIdNotMatchFailure=Failed to {0} resource with match URL "{1}" because the matching resource does not match the provided ID ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedNoId=Failed to {0} resource in transaction because no ID was provided ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.transactionOperationFailedUnknownId=Failed to {0} resource in transaction because no resource could be found with ID {1} @@ -210,3 +211,5 @@ ca.uhn.fhir.jpa.provider.DiffProvider.cantDiffDifferentTypes=Unable to diff two ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.noMatchingProfile=Resource of type "{0}" does not declare conformance to profile from: {1} ca.uhn.fhir.jpa.interceptor.validation.RuleRequireProfileDeclaration.illegalProfile=Resource of type "{0}" must not declare conformance to profile: {1} + +ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl.invalidUseOfSearchIdentifier=Unsupported search modifier(s): "{0}" for resource type "{1}". Valid search modifiers are: {2} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/JsonUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/JsonUtilTest.java new file mode 100644 index 00000000000..7d0adfaeea1 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/JsonUtilTest.java @@ -0,0 +1,54 @@ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.model.api.IModelJson; +import ca.uhn.fhir.model.api.annotation.SensitiveNoDisplay; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +class JsonUtilTest { + + @JsonFilter(IModelJson.SENSITIVE_DATA_FILTER_NAME) + class TestObject implements IModelJson { + @JsonProperty("sensitiveField") + @SensitiveNoDisplay + private String mySensitiveField; + + @JsonProperty(value = "publicField") + private String myPublicField; + + public String getPrivateField() { + return mySensitiveField; + } + + public void setSensitiveField(String thePrivateField) { + this.mySensitiveField = thePrivateField; + } + + public String getPublicField() { + return myPublicField; + } + + public void setPublicField(String thePublicField) { + this.myPublicField = thePublicField; + } + } + + @Test + public void testSensitiveNoDisplayAnnotationIsHiddenFromBasicSerialization() { + TestObject object = new TestObject(); + object.setPublicField("Public Value!"); + object.setSensitiveField("Sensitive Value!"); + + String sensitiveExcluded = JsonUtil.serializeOrInvalidRequest(object); + assertThat(sensitiveExcluded, is(not(containsString("Sensitive Value!")))); + + String sensitiveIncluded = JsonUtil.serializeWithSensitiveData(object); + assertThat(sensitiveIncluded, is(containsString("Sensitive Value!"))); + } +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java index fd96f2cf4ad..412662a972b 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.junit.jupiter.api.Test; @@ -12,7 +13,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class ValidateUtilTest { @Test - public void testValidate() { + public void testIsTrueOrThrowInvalidRequest() { ValidateUtil.isTrueOrThrowInvalidRequest(true, ""); try { @@ -23,6 +24,18 @@ public class ValidateUtilTest { } } + @Test + public void testIsTrueOrThrowResourceNotFound() { + ValidateUtil.isTrueOrThrowResourceNotFound(true, ""); + + try { + ValidateUtil.isTrueOrThrowResourceNotFound(false, "The message"); + fail(); + } catch (ResourceNotFoundException e) { + assertEquals(Msg.code(2494) + "The message", e.getMessage()); + } + } + @Test public void testIsGreaterThan() { ValidateUtil.isGreaterThan(2L, 1L, ""); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionEnumTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionEnumTest.java index b0d8c5bf8a4..7e965b6b4fc 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionEnumTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionEnumTest.java @@ -29,7 +29,7 @@ public class VersionEnumTest { int minor = Integer.parseInt(parts[1]); int patch = Integer.parseInt(parts[2]); - if (major >= 6 && minor >= 3) { + if ((major == 6 && minor >= 3) || (major >= 7)) { if (minor % 2 == 1) { patch = 0; } diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index d50ca6d1cee..2040ddf8375 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,7 +4,7 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT pom HAPI FHIR BOM @@ -12,7 +12,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 969b20ccbfc..ef72ad8b93e 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 780c0ffcfa3..bf3f1849c84 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/BulkImportCommandTest.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/BulkImportCommandTest.java index db1ea7036ab..aa535c48919 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/BulkImportCommandTest.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/BulkImportCommandTest.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -106,6 +107,7 @@ public class BulkImportCommandTest { writeNdJsonFileToTempDirectory(fileContents1, "file1.json"); writeNdJsonFileToTempDirectory(fileContents2, "file2.json"); + when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation(any(), any())).thenReturn(RequestPartitionId.allPartitions()); when(myJobCoordinator.startInstance(any(), any())).thenReturn(createJobStartResponse("THE-JOB-ID")); // Start the command in a separate thread @@ -149,6 +151,7 @@ public class BulkImportCommandTest { when(myJobCoordinator.startInstance(any(), any())) .thenReturn(createJobStartResponse("THE-JOB-ID")); + when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation(any(), any())).thenReturn(RequestPartitionId.allPartitions()); // Start the command in a separate thread new Thread(() -> App.main(new String[]{ @@ -189,6 +192,7 @@ public class BulkImportCommandTest { writeNdJsonFileToTempDirectory(fileContents1, "file1.json"); writeNdJsonFileToTempDirectory(fileContents2, "file2.json"); + when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation(any(), any())).thenReturn(RequestPartitionId.allPartitions()); when(myJobCoordinator.startInstance(any(), any())).thenReturn(createJobStartResponse("THE-JOB-ID")); // Start the command in a separate thread diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index b9aba8e0b26..75334f3883b 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 330133c4242..91a0189c1eb 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 988e0dcc301..84d15d21570 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index 973441a353c..e0f3c19e598 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 85faf8a590d..5a5c4d85db8 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 4586527506f..f032f352acf 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index e3d991624a0..faadca38e5e 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5537-attachment-util-content-type-fix.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5537-attachment-util-content-type-fix.yaml new file mode 100644 index 00000000000..6a72835bcc0 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5537-attachment-util-content-type-fix.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5537 +title: "Calling the method getOrCreateContentType in AttachmentUtil on an attachment with no content type would throw exception because contentType is a code not a string. +This fixes the function to create an empty code as expected" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5547-collation-index-fix.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5547-collation-index-fix.yaml new file mode 100644 index 00000000000..c6ab2f02556 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5547-collation-index-fix.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5547 +title: "The addition of the indexes `idx_sp_uri_hash_identity_pattern_ops` and `idx_sp_string_hash_nrm_pattern_ops` could occasionally timeout during migration in Postgresql on large databases, leaving the migration table in a failed state, and Smile CDR unable to boot. +Now existence of the index is checked before attempting to add it again." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5603-is-a-descendent-of.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5603-is-a-descendent-of.yaml new file mode 100644 index 00000000000..1c8244a07ee --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5603-is-a-descendent-of.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5603 +jira: SMILE-8000 +title: "Previously, the semantics of `is-a` were incorrect in Valueset Expansion. The implementation previously used the behaviour of `descendent-of`, which means that `A is-a A` was not being considered as true. This has been corrected. In addition, +`descendent-of` is now supported, which compares for strict descendency, and does not include itself. Thanks to Ole Hedegaard (@ohetrifork) for the fix." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5619-fix-auto-version-reference-for-conditional-update-with-id-placeholder.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5619-fix-auto-version-reference-for-conditional-update-with-id-placeholder.yaml new file mode 100644 index 00000000000..3077b0e9595 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5619-fix-auto-version-reference-for-conditional-update-with-id-placeholder.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 5619 +jira: SMILE-7909 +title: "Previously, when a transaction was posted with a resource that had placeholder references and auto versioning +references enabled for that path, if the target resource was included in the Bundle but not modified, the reference was +saved with a version number that didn't exist. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5621-fix-potential-deadlock-on-caffeine-cash-on-conditional-create.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5621-fix-potential-deadlock-on-caffeine-cash-on-conditional-create.yaml new file mode 100644 index 00000000000..5fb4b901ffe --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5621-fix-potential-deadlock-on-caffeine-cash-on-conditional-create.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 5621 +title: "Fixed a deadlock in resource conditional create." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5623-searching-with-multiple-bundle-composition-searchparameters-fix.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5623-searching-with-multiple-bundle-composition-searchparameters-fix.yaml new file mode 100644 index 00000000000..1041359ccf0 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5623-searching-with-multiple-bundle-composition-searchparameters-fix.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5623 +title: "Previously, searches that used more than one chained `Bundle` `SearchParameter` (i.e. `Composition`) were only +adding one condition to the underlying SQL query which resulted in incorrect search results. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5626-fix-potential-exception-thrown-on-eventlistener.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5626-fix-potential-exception-thrown-on-eventlistener.yaml new file mode 100644 index 00000000000..88e38416824 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5626-fix-potential-exception-thrown-on-eventlistener.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5626 +title: "Previously, an exception could be thrown by the container when executing a contextClosedEvent on the + Scheduler Service. This issue has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5632-bulk-export-response-must-have-required-fields.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5632-bulk-export-response-must-have-required-fields.yaml new file mode 100644 index 00000000000..cc207611c3f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5632-bulk-export-response-must-have-required-fields.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5632 +title: "Previously bulk export operation was returning an empty response when no resources matched the request, which + didn't comply with [HL7 HAPI IG](https://hl7.org/fhir/uv/bulkdata/export/index.html#response---complete-status). + This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5633-oracle-hfj-res-ver-clob-migration.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5633-oracle-hfj-res-ver-clob-migration.yaml new file mode 100644 index 00000000000..08edbe2b2b7 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5633-oracle-hfj-res-ver-clob-migration.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5633 +title: "Smile failed to save resources running on Oracle when installed from 2023-02 or earlier. + This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5634-failure-expanding-valueset-no-concepts-with-mimetype-system.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5634-failure-expanding-valueset-no-concepts-with-mimetype-system.yaml new file mode 100644 index 00000000000..89f6c87c2b4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5634-failure-expanding-valueset-no-concepts-with-mimetype-system.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5634 +title: "Previously, expanding a 'ValueSet' with no concepts based on system `urn:ietf:bcp:13` would fail with +`ExpansionCouldNotBeCompletedInternallyException`. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5636-fix-expunge-oparation-ignore-thread-count-setting.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5636-fix-expunge-oparation-ignore-thread-count-setting.yaml new file mode 100644 index 00000000000..e9ecac20056 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5636-fix-expunge-oparation-ignore-thread-count-setting.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 5636 +jira: SMILE-7648 +title: "Previously, the number of threads allocated to the $expunge operation in certain cases could be more +than configured, this would cause hundreds of threads to be created and all available database connections +to be consumed. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5640-measurescore-popid-nullpointer-bug.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5640-measurescore-popid-nullpointer-bug.yaml new file mode 100644 index 00000000000..7f7d5778332 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5640-measurescore-popid-nullpointer-bug.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5640 +jira: SMILE-7977 +title: "Clinical reasoning version bump to address reported 'null pointer' error that is encountered when running $evaluate-measure against a measure with an omitted measure.group.population.id" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5642-bundle-patch-cant-handle-transaction-nested-paramters.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5642-bundle-patch-cant-handle-transaction-nested-paramters.yaml new file mode 100644 index 00000000000..129e6282523 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5642-bundle-patch-cant-handle-transaction-nested-paramters.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5642 +title: "A non-superuser with correct permissions encounters HAPI-0339 when POSTING a transaction Bundle with a PATCH. + This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5644-searching-for-bundles-with-read-all-bundles-permissions-returns-403.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5644-searching-for-bundles-with-read-all-bundles-permissions-returns-403.yaml new file mode 100644 index 00000000000..ee53104bf80 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5644-searching-for-bundles-with-read-all-bundles-permissions-returns-403.yaml @@ -0,0 +1,8 @@ +--- +type: fix +issue: 5644 +title: "Previously, searching for `Bundle` resources with read all `Bundle` resources permissions, returned an +HTTP 403 Forbidden error. This was because the `AuthorizationInterceptor` applied permissions to the resources inside +the `Bundle`, instead of the `Bundle` itself. This has been fixed and permissions are no longer applied to the resources +inside a `Bundle` of type `document`, `message`, or `collection` for `Bundle` requests." + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5649-index-review.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5649-index-review.yaml new file mode 100644 index 00000000000..bdd4182bde9 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5649-index-review.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 5649 +title: "Change database upgrade script to avoid holding locks while adding indices." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5651-bundle-conditional-create-reference-query-string.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5651-bundle-conditional-create-reference-query-string.yaml new file mode 100644 index 00000000000..df8d742600e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5651-bundle-conditional-create-reference-query-string.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5651 +jira: SMILE-7855 +title: "Previously, conditional creates would fail with HAPI-0929 errors if there was no preceding '?'. + This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5656-bulk-import-credentials.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5656-bulk-import-credentials.yaml new file mode 100644 index 00000000000..866f8c7bc5a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5656-bulk-import-credentials.yaml @@ -0,0 +1,5 @@ +--- +type: fix +jira: SMILE-7216 +title: "Previously, the Bulk Import (`$import`) job was ignoring the `httpBasicCredentials` section of the incoming parameters +object, causing the job to fail with a 403 error. This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5659-npe-system-bulk-export-patientid-interceptor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5659-npe-system-bulk-export-patientid-interceptor.yaml new file mode 100644 index 00000000000..ea41710dc9c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5659-npe-system-bulk-export-patientid-interceptor.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5659 +title: "Previously, after registering built-in interceptor `PatientIdPartitionInterceptor`, the system bulk export +(with no filters) operation would fail with a NullPointerException. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5110-failure-in-transaction-with-empty-system.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5110-failure-in-transaction-with-empty-system.yaml new file mode 100644 index 00000000000..c1bb92bcfd4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5110-failure-in-transaction-with-empty-system.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5110 +title: "When processing a FHIR transaction in the JPA server, an identifier containing a + system that has no value but has an extension present could cause a NullPointerException. + This has been corrected." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5667-fix-parsing-xml-encoded-fhir-resource-with-leading-plus-decimal-value.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5667-fix-parsing-xml-encoded-fhir-resource-with-leading-plus-decimal-value.yaml new file mode 100644 index 00000000000..88d8e20420a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5667-fix-parsing-xml-encoded-fhir-resource-with-leading-plus-decimal-value.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5667 +title: "Previously, creating an XML encoded FHIR resource with a decimal element that has a leading plus sign value +would result in `JsonParseException` during the read operation from the database. Thus, making it impossible to +retrieve or modify such resources. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5668-chained-sort-near.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5668-chained-sort-near.yaml new file mode 100644 index 00000000000..56e06ca402a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5668-chained-sort-near.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 5668 +title: "Added support for sorting on a chained `location.near` search. This allows you to sort location by nearness via a chained search. Thanks to Nicolai Gjøderum (@nigtrifork) for the contribution!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5671-tx-tangle.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5671-tx-tangle.yaml new file mode 100644 index 00000000000..0224e092a74 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5671-tx-tangle.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 5671 +title: "Avoid lock contention by refreshing SearchParameter cache in a new transaction." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5672-search-non-chained-reference-invalid-resource-type-should-throw-exception.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5672-search-non-chained-reference-invalid-resource-type-should-throw-exception.yaml new file mode 100644 index 00000000000..c039d9faf14 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5672-search-non-chained-reference-invalid-resource-type-should-throw-exception.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5672 +title: "Previously, when performing a FHIR search using a non-chained relative reference (returns entire resource) with +a server assigned id, it ignores the invalid resourceType in the parameter value and proceeds with the id based lookup. e.g. + GET `/MedicationAdministration?context=abc/1352` returns `Encounter/1352`. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml new file mode 100644 index 00000000000..7304616964a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5682 +title: "The BundleBuilder utility class will no longer include the `/_version/xxx` portion of the + resource ID in the `Bundle.entry.fullUrl` it generates, as the FHIR specification states that this + should be omitted." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml new file mode 100644 index 00000000000..9af888831bf --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml @@ -0,0 +1,9 @@ +--- +type: change +issue: 5682 +title: "The IPS $summary generation API has been overhauled to make it more flexible for + future use cases. Specifically, the section registry has been removed and folded into + the generation strategy, and support has been added for non-JPA sources of data. This is + a breaking change to the API, and implementers will need to update their code. This updated + API incorporates community feedback, and should now be considered a stable API for IPS + generation." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml new file mode 100644 index 00000000000..80b29d1fb39 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml @@ -0,0 +1,26 @@ +--- +type: add +issue: 5682 +title: "Several enhancements have been made to the International Patient Summary generator based on + feedback from implementers: +
    +
  • + New methods have been added to the IIpsGenerationStrategy allowing resources + for any or all sections to be fetched from a source other than the FHIR repository. +
  • +
  • + The IpsSectionEnum class has been removed and replaced in any user-facing APIs + with references to SectionRegistry.Section. This makes it much easier to + extend or replace the section registry with custom sections not defined in the universal + IPS implementation guide. +
  • +
  • + Captions have been removed from narrative section tables, and replaced with H5 tags + directly above the table. This results in an easier to read display since the table + title will appear above the table instead of below it. +
  • +
  • + The IPS narrative generator built in templates will now omit tables when the template + specified multiple tables and the specific table would have no resources. +
  • +
" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml new file mode 100644 index 00000000000..ef6bca8549a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5682 +title: "The IPS Generator will no longer replace resource IDs with placeholder IDs in the resulting + bundle by default, although this can be overridden in the generation strategy object." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5690-delete-by-url-limit-number-resolved-resource-ids.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5690-delete-by-url-limit-number-resolved-resource-ids.yaml new file mode 100644 index 00000000000..a59c1753baa --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5690-delete-by-url-limit-number-resolved-resource-ids.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 5690 +title: "Previously, a DELETE on a specific URL search string would always attempt to delete no matter the number of + resolved resources. + This has been fixed by adding a storage setting to enforce a threshold for resolved resources, above which + the DELETE operation will fail to execute with HAPI-2496." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5701-search-by-identifier-inconsistent-results.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5701-search-by-identifier-inconsistent-results.yaml new file mode 100644 index 00000000000..0ec9cea92af --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5701-search-by-identifier-inconsistent-results.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5701 +title: "Previously, invoking search URLs containing ':identifier' would result in a HAPI-1250 error complaining about + an invalid resource type. + This has been fixed by returning a clearer error message for this specific condition: HAPI-2498." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5707-structure-definition-cache-not-refreshed-with-new-entry.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5707-structure-definition-cache-not-refreshed-with-new-entry.yaml new file mode 100644 index 00000000000..5f2defaf100 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5707-structure-definition-cache-not-refreshed-with-new-entry.yaml @@ -0,0 +1,8 @@ +--- +type: fix +issue: 5707 +jira: SMILE-7270 +title: "Previously, with validation active, when a user POSTed a resource with a meta profile with a non-existent + StructureDefinition URL, then POSTed the StructureDefinition, POSTing the same or another patient with that same + meta profile URL would still fail with a VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY validation error. + This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md index 13362b43668..aec6fdbba94 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md @@ -14,24 +14,17 @@ The IPS Generator uses FHIR resources stored in your repository as its input. Th # Generation Strategy -A user supplied strategy class is used to determine various properties of the IPS. This class must implement the `IIpsGenerationStrategy` interface. A default implementation called `DefaultIpsGenerationStrategy` is included. You may use this default implementation, use a subclassed version of it that adds additional logic, or use en entirely new implementation. +A user supplied strategy class is used to determine various properties of the IPS. This class must implement the `IIpsGenerationStrategy` interface. A default implementation called `DefaultJpaIpsGenerationStrategy` is included. You may use this default implementation, use a subclassed version of it that adds additional logic, or use en entirely new implementation. -The generation strategy also supplies the [Section Registry](#section-registry) and [Narrative Templates](#narrative-templates) implementations, so it can be considered the central part of your IPS configuration. +The generation strategy also supplies the [Narrative Templates](#narrative-templates) implementations, so it can be considered the central part of your IPS configuration. * JavaDoc: [IIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.html) * Source Code: [IIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java) -* JavaDoc: [DefaultIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.html) -* Source Code: [DefaultIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java) +The default generation strategy defines the sections that will be included in your IPS. Out of the box, the standard IPS sections are all included. See the [IG homepage](http://hl7.org/fhir/uv/ips/) for a list of the standard sections. - - -# Section Registry - -The IPS SectionRegistry class defines the sections that will be included in your IPS. Out of the box, the standard IPS sections are all included. See the [IG homepage](http://hl7.org/fhir/uv/ips/) for a list of the standard sections. - -* JavaDoc: [SectionRegistry](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/api/SectionRegistry.html) -* Source Code: [SectionRegistry.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java) +* JavaDoc: [DefaultJpaIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.html) +* Source Code: [DefaultJpaIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java) @@ -44,7 +37,7 @@ The IPS generator uses HAPI FHIR [Narrative Generation](/hapi-fhir/docs/model/na Narrative templates for individual sections will be supplied a Bundle resource containing only the matched resources for the individual section as entries (ie. the Composition itself will not be present and no other resources will be present). So, for example, when generating the _Allergies / Intolerances_ IPS section narrative, the input to the narrative generator will be a _Bundle_ resource containing only _AllergyIntolerance_ resources. -The narrative properties file should contain definitions using the profile URL of the individual section (as defined in the [section registry](#section-registry)) as the `.profile` qualifier. For example: +The narrative properties file should contain definitions using the profile URL of the individual section (as defined in the section definition within the generation strategy) as the `.profile` qualifier. For example: ```properties ips-allergyintolerance.resourceType=Bundle diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 160ca4ca85d..6622e41d7f4 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index af754c56fae..86c000d43f8 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index e6e48a8e727..c079ae4b15d 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/sched/BaseSchedulerServiceImpl.java b/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/sched/BaseSchedulerServiceImpl.java index 9d59f8238d3..358aa408176 100644 --- a/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/sched/BaseSchedulerServiceImpl.java +++ b/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/sched/BaseSchedulerServiceImpl.java @@ -28,13 +28,13 @@ import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; import ca.uhn.fhir.util.StopWatch; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.quartz.JobKey; import org.quartz.SchedulerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.env.Environment; @@ -177,7 +177,7 @@ public abstract class BaseSchedulerServiceImpl implements ISchedulerService { values.forEach(t -> t.scheduleJobs(this)); } - @EventListener(ContextClosedEvent.class) + @PreDestroy public void stop() { ourLog.info("Shutting down task scheduler..."); diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 9dbfffd390e..a740fa164ff 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HibernatePropertiesProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HibernatePropertiesProvider.java index 74702646d6b..38d3e6747d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HibernatePropertiesProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HibernatePropertiesProvider.java @@ -75,4 +75,8 @@ public class HibernatePropertiesProvider { public DataSource getDataSource() { return myEntityManagerFactory.getDataSource(); } + + public boolean isOracleDialect() { + return getDialect() instanceof org.hibernate.dialect.OracleDialect; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 422f5a86f01..e60974ae642 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -51,6 +51,7 @@ import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.JpaStorageResourceParser; import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; +import ca.uhn.fhir.jpa.dao.ResourceHistoryCalculator; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.data.IResourceModifiedDao; @@ -869,4 +870,10 @@ public class JpaConfig { public IMetaTagSorter metaTagSorter() { return new MetaTagSorterAlphabetical(); } + + @Bean + public ResourceHistoryCalculator resourceHistoryCalculator( + FhirContext theFhirContext, HibernatePropertiesProvider theHibernatePropertiesProvider) { + return new ResourceHistoryCalculator(theFhirContext, theHibernatePropertiesProvider.isOracleDialect()); + } } 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 c8c6a3fc9fb..06200fc852a 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 @@ -85,7 +85,6 @@ import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; @@ -105,8 +104,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.collect.Sets; import com.google.common.hash.HashCode; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; @@ -264,6 +261,9 @@ public abstract class BaseHapiFhirDao extends BaseStora @Autowired private PlatformTransactionManager myTransactionManager; + @Autowired + protected ResourceHistoryCalculator myResourceHistoryCalculator; + protected final CodingSpy myCodingSpy = new CodingSpy(); @VisibleForTesting @@ -277,6 +277,11 @@ public abstract class BaseHapiFhirDao extends BaseStora mySearchParamPresenceSvc = theSearchParamPresenceSvc; } + @VisibleForTesting + public void setResourceHistoryCalculator(ResourceHistoryCalculator theResourceHistoryCalculator) { + myResourceHistoryCalculator = theResourceHistoryCalculator; + } + @Override protected IInterceptorBroadcaster getInterceptorBroadcaster() { return myInterceptorBroadcaster; @@ -643,6 +648,7 @@ public abstract class BaseHapiFhirDao extends BaseStora theEntity.setResourceType(toResourceName(theResource)); } + byte[] resourceBinary; String resourceText; ResourceEncodingEnum encoding; boolean changed = false; @@ -659,6 +665,7 @@ public abstract class BaseHapiFhirDao extends BaseStora if (address != null) { encoding = ResourceEncodingEnum.ESR; + resourceBinary = null; resourceText = address.getProviderId() + ":" + address.getLocation(); changed = true; @@ -675,10 +682,15 @@ public abstract class BaseHapiFhirDao extends BaseStora theEntity.setFhirVersion(myContext.getVersion().getVersion()); - HashFunction sha256 = Hashing.sha256(); - resourceText = encodeResource(theResource, encoding, excludeElements, myContext); - encoding = ResourceEncodingEnum.JSON; - HashCode hashCode = sha256.hashUnencodedChars(resourceText); + // TODO: LD: Once 2024-02 it out the door we should consider further refactoring here to move + // more of this logic within the calculator and eliminate more local variables + final ResourceHistoryState calculate = myResourceHistoryCalculator.calculateResourceHistoryState( + theResource, encoding, excludeElements); + + resourceText = calculate.getResourceText(); + resourceBinary = calculate.getResourceBinary(); + encoding = calculate.getEncoding(); // This may be a no-op + final HashCode hashCode = calculate.getHashCode(); String hashSha256 = hashCode.toString(); if (!hashSha256.equals(theEntity.getHashSha256())) { @@ -696,6 +708,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } else { encoding = null; + resourceBinary = null; resourceText = null; } @@ -713,6 +726,7 @@ public abstract class BaseHapiFhirDao extends BaseStora changed = true; } + resourceBinary = null; resourceText = null; encoding = ResourceEncodingEnum.DEL; } @@ -737,13 +751,17 @@ public abstract class BaseHapiFhirDao extends BaseStora if (currentHistoryVersion == null || !currentHistoryVersion.hasResource()) { changed = true; } else { - changed = !StringUtils.equals(currentHistoryVersion.getResourceTextVc(), resourceText); + // TODO: LD: Once 2024-02 it out the door we should consider further refactoring here to move + // more of this logic within the calculator and eliminate more local variables + changed = myResourceHistoryCalculator.isResourceHistoryChanged( + currentHistoryVersion, resourceBinary, resourceText); } } } EncodedResource retVal = new EncodedResource(); retVal.setEncoding(encoding); + retVal.setResourceBinary(resourceBinary); retVal.setResourceText(resourceText); retVal.setChanged(changed); @@ -1393,8 +1411,11 @@ public abstract class BaseHapiFhirDao extends BaseStora ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding(); List excludeElements = new ArrayList<>(8); getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta()); - String encodedResourceString = encodeResource(theResource, encoding, excludeElements, myContext); - boolean changed = !StringUtils.equals(historyEntity.getResourceTextVc(), encodedResourceString); + String encodedResourceString = + myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements); + byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString); + final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged( + historyEntity, resourceBinary, encodedResourceString); historyEntity.setUpdated(theTransactionDetails.getTransactionDate()); @@ -1406,14 +1427,15 @@ public abstract class BaseHapiFhirDao extends BaseStora return historyEntity; } - populateEncodedResource(encodedResource, encodedResourceString, ResourceEncodingEnum.JSON); + myResourceHistoryCalculator.populateEncodedResource( + encodedResource, encodedResourceString, resourceBinary, encoding); } - /* * Save the resource itself to the resourceHistoryTable */ historyEntity = myEntityManager.merge(historyEntity); historyEntity.setEncoding(encodedResource.getEncoding()); + historyEntity.setResource(encodedResource.getResourceBinary()); historyEntity.setResourceTextVc(encodedResource.getResourceText()); myResourceHistoryTableDao.save(historyEntity); @@ -1423,8 +1445,12 @@ public abstract class BaseHapiFhirDao extends BaseStora } private void populateEncodedResource( - EncodedResource encodedResource, String encodedResourceString, ResourceEncodingEnum theEncoding) { + EncodedResource encodedResource, + String encodedResourceString, + byte[] theResourceBinary, + ResourceEncodingEnum theEncoding) { encodedResource.setResourceText(encodedResourceString); + encodedResource.setResourceBinary(theResourceBinary); encodedResource.setEncoding(theEncoding); } @@ -1489,6 +1515,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } historyEntry.setEncoding(theChanged.getEncoding()); + historyEntry.setResource(theChanged.getResourceBinary()); historyEntry.setResourceTextVc(theChanged.getResourceText()); ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId()); @@ -1926,16 +1953,6 @@ public abstract class BaseHapiFhirDao extends BaseStora return resourceText; } - public static String encodeResource( - IBaseResource theResource, - ResourceEncodingEnum theEncoding, - List theExcludeElements, - FhirContext theContext) { - IParser parser = theEncoding.newParser(theContext); - parser.setDontEncodeElements(theExcludeElements); - return parser.encodeResourceToString(theResource); - } - private static String parseNarrativeTextIntoWords(IBaseResource theResource) { StringBuilder b = new StringBuilder(); 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 9fada93f0db..b86c9e2cb53 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 @@ -30,7 +30,6 @@ import ca.uhn.fhir.i18n.Msg; 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.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; @@ -852,12 +851,15 @@ public abstract class BaseHapiFhirResourceDao extends B return deleteExpunge(theUrl, theRequest); } - return myTransactionService.execute(theRequest, transactionDetails, tx -> { - DeleteConflictList deleteConflicts = new DeleteConflictList(); - DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest, transactionDetails); - DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); - return outcome; - }); + return myTransactionService + .withRequest(theRequest) + .withTransactionDetails(transactionDetails) + .execute(tx -> { + DeleteConflictList deleteConflicts = new DeleteConflictList(); + DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest, transactionDetails); + DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); + return outcome; + }); } /** @@ -872,10 +874,10 @@ public abstract class BaseHapiFhirResourceDao extends B @Nonnull TransactionDetails theTransactionDetails) { validateDeleteEnabled(); - return myTransactionService.execute( - theRequestDetails, - theTransactionDetails, - tx -> doDeleteByUrl(theUrl, deleteConflicts, theTransactionDetails, theRequestDetails)); + return myTransactionService + .withRequest(theRequestDetails) + .withTransactionDetails(theTransactionDetails) + .execute(tx -> doDeleteByUrl(theUrl, deleteConflicts, theTransactionDetails, theRequestDetails)); } @Nonnull @@ -902,6 +904,19 @@ public abstract class BaseHapiFhirResourceDao extends B theUrl, resourceIds.size())); } + // TODO: LD: There is a still a bug on slow deletes: https://github.com/hapifhir/hapi-fhir/issues/5675 + final long threshold = getStorageSettings().getRestDeleteByUrlResourceIdThreshold(); + if (resourceIds.size() > threshold) { + throw new PreconditionFailedException(Msg.code(2496) + + getContext() + .getLocalizer() + .getMessageSanitized( + BaseStorageDao.class, + "deleteByUrlThresholdExceeded", + theUrl, + resourceIds.size(), + threshold)); + } } return deletePidList(theUrl, resourceIds, deleteConflicts, theRequestDetails, theTransactionDetails); @@ -1233,9 +1248,9 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { StopWatch w = new StopWatch(); - ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(myResourceName, null); RequestPartitionId requestPartitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details); + myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( + theRequestDetails, myResourceName, null); IBundleProvider retVal = myTransactionService .withRequest(theRequestDetails) .withRequestPartitionId(requestPartitionId) @@ -1254,9 +1269,9 @@ public abstract class BaseHapiFhirResourceDao extends B final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) { StopWatch w = new StopWatch(); - ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(myResourceName, theId); RequestPartitionId requestPartitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, details); + myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( + theRequest, myResourceName, theId); IBundleProvider retVal = myTransactionService .withRequest(theRequest) .withRequestPartitionId(requestPartitionId) @@ -1284,9 +1299,9 @@ public abstract class BaseHapiFhirResourceDao extends B final HistorySearchDateRangeParam theHistorySearchDateRangeParam, RequestDetails theRequest) { StopWatch w = new StopWatch(); - ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(myResourceName, theId); RequestPartitionId requestPartitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, details); + myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( + theRequest, myResourceName, theId); IBundleProvider retVal = myTransactionService .withRequest(theRequest) .withRequestPartitionId(requestPartitionId) @@ -1333,10 +1348,9 @@ public abstract class BaseHapiFhirResourceDao extends B addAllResourcesTypesToReindex(theBase, theRequestDetails, params); } - ReadPartitionIdRequestDetails details = - ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_REINDEX); RequestPartitionId requestPartition = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details); + myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.OPERATION_REINDEX); params.setRequestPartitionId(requestPartition); JobInstanceStartRequest request = new JobInstanceStartRequest(); @@ -1710,17 +1724,11 @@ public abstract class BaseHapiFhirResourceDao extends B if (historyEntity.getEncoding() == ResourceEncodingEnum.JSONC || historyEntity.getEncoding() == ResourceEncodingEnum.JSON) { byte[] resourceBytes = historyEntity.getResource(); - - // Always migrate data out of the bytes column if (resourceBytes != null) { String resourceText = decodeResource(resourceBytes, historyEntity.getEncoding()); - ourLog.debug( - "Storing text of resource {} version {} as inline VARCHAR", - entity.getResourceId(), - historyEntity.getVersion()); - historyEntity.setResourceTextVc(resourceText); - historyEntity.setEncoding(ResourceEncodingEnum.JSON); - changed = true; + if (myResourceHistoryCalculator.conditionallyAlterHistoryEntity(entity, historyEntity, resourceText)) { + changed = true; + } } } if (isBlank(historyEntity.getSourceUri()) && isBlank(historyEntity.getRequestId())) { @@ -1966,7 +1974,7 @@ public abstract class BaseHapiFhirResourceDao extends B RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( - theRequest, getResourceName(), theParams, null); + theRequest, getResourceName(), theParams); IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch( this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId); @@ -2135,7 +2143,7 @@ public abstract class BaseHapiFhirResourceDao extends B BiFunction, Stream> transform) { RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( - theRequest, myResourceName, theParams, null); + theRequest, myResourceName, theParams); String uuid = UUID.randomUUID().toString(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index b996add0d0e..3875153f138 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; -import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; @@ -158,9 +157,9 @@ public abstract class BaseHapiFhirSystemDao extends B @Override public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { StopWatch w = new StopWatch(); - ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(null, null); RequestPartitionId requestPartitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details); + myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( + theRequestDetails, null, null); IBundleProvider retVal = myTransactionService .withRequest(theRequestDetails) .withRequestPartitionId(requestPartitionId) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java index d1d85f77727..cccce26e226 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; class EncodedResource { private boolean myChanged; + private byte[] myResource; private ResourceEncodingEnum myEncoding; private String myResourceText; @@ -35,6 +36,14 @@ class EncodedResource { myEncoding = theEncoding; } + public byte[] getResourceBinary() { + return myResource; + } + + public void setResourceBinary(byte[] theResource) { + myResource = theResource; + } + public boolean isChanged() { return myChanged; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index c05a09a3b7d..f9b62ad95e9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -91,6 +91,9 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport // TermReadSvcImpl calls these methods as a part of its "isCodeSystemSupported" calls. // We should modify CachingValidationSupport to cache the results of "isXXXSupported" // at which point we could do away with this cache + // TODO: LD: This cache seems to supersede the cache in CachingValidationSupport, as that cache is set to + // 10 minutes, but this 1 minute cache now determines the expiry. + // This new behaviour was introduced between the 7.0.0 release and the current master (7.2.0) private Cache myLoadCache = CacheFactory.build(TimeUnit.MINUTES.toMillis(1), 1000); /** @@ -188,6 +191,9 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport IBaseResource fetched = myLoadCache.get(key, t -> doFetchResource(theClass, theUri)); if (fetched == myNoMatch) { + ourLog.debug( + "Invalidating cache entry for URI: {} since the result of the underlying query is empty", theUri); + myLoadCache.invalidate(key); return null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoObservation.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoObservation.java index ea006a3efd0..69088bc9699 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoObservation.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoObservation.java @@ -65,7 +65,7 @@ public class JpaResourceDaoObservation extends BaseHapi RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( - theRequestDetails, getResourceName(), theSearchParameterMap, null); + theRequestDetails, getResourceName(), theSearchParameterMap); return mySearchCoordinatorSvc.registerSearch( this, theSearchParameterMap, @@ -128,7 +128,7 @@ public class JpaResourceDaoObservation extends BaseHapi RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( - theRequestDetails, getResourceName(), theSearchParameterMap, null); + theRequestDetails, getResourceName(), theSearchParameterMap); List> patientParams = new ArrayList<>(); if (theSearchParameterMap.get(getPatientParamName()) != null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoPatient.java index 119b3d04d1b..806674cb0d7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaResourceDaoPatient.java @@ -106,7 +106,7 @@ public class JpaResourceDaoPatient extends BaseHapiFhir } RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType( - theRequest, getResourceName(), paramMap, null); + theRequest, getResourceName(), paramMap); adjustCount(theRequest, paramMap); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceHistoryCalculator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceHistoryCalculator.java new file mode 100644 index 00000000000..1114af1a887 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceHistoryCalculator.java @@ -0,0 +1,153 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.dao; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.parser.IParser; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +/** + * Responsible for various resource history-centric and {@link FhirContext} aware operations called by + * {@link BaseHapiFhirDao} or {@link BaseHapiFhirResourceDao} that require knowledge of whether an Oracle database is + * being used. + */ +public class ResourceHistoryCalculator { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceHistoryCalculator.class); + private static final HashFunction SHA_256 = Hashing.sha256(); + + private final FhirContext myFhirContext; + private final boolean myIsOracleDialect; + + public ResourceHistoryCalculator(FhirContext theFhirContext, boolean theIsOracleDialect) { + myFhirContext = theFhirContext; + myIsOracleDialect = theIsOracleDialect; + } + + ResourceHistoryState calculateResourceHistoryState( + IBaseResource theResource, ResourceEncodingEnum theEncoding, List theExcludeElements) { + final String encodedResource = encodeResource(theResource, theEncoding, theExcludeElements); + final byte[] resourceBinary; + final String resourceText; + final ResourceEncodingEnum encoding; + final HashCode hashCode; + + if (myIsOracleDialect) { + resourceText = null; + resourceBinary = getResourceBinary(theEncoding, encodedResource); + encoding = theEncoding; + hashCode = SHA_256.hashBytes(resourceBinary); + } else { + resourceText = encodedResource; + resourceBinary = null; + encoding = ResourceEncodingEnum.JSON; + hashCode = SHA_256.hashUnencodedChars(encodedResource); + } + + return new ResourceHistoryState(resourceText, resourceBinary, encoding, hashCode); + } + + boolean conditionallyAlterHistoryEntity( + ResourceTable theEntity, ResourceHistoryTable theHistoryEntity, String theResourceText) { + if (!myIsOracleDialect) { + ourLog.debug( + "Storing text of resource {} version {} as inline VARCHAR", + theEntity.getResourceId(), + theHistoryEntity.getVersion()); + theHistoryEntity.setResourceTextVc(theResourceText); + theHistoryEntity.setResource(null); + theHistoryEntity.setEncoding(ResourceEncodingEnum.JSON); + return true; + } + + return false; + } + + boolean isResourceHistoryChanged( + ResourceHistoryTable theCurrentHistoryVersion, + @Nullable byte[] theResourceBinary, + @Nullable String resourceText) { + if (myIsOracleDialect) { + return !Arrays.equals(theCurrentHistoryVersion.getResource(), theResourceBinary); + } + + return !StringUtils.equals(theCurrentHistoryVersion.getResourceTextVc(), resourceText); + } + + String encodeResource( + IBaseResource theResource, ResourceEncodingEnum theEncoding, List theExcludeElements) { + final IParser parser = theEncoding.newParser(myFhirContext); + parser.setDontEncodeElements(theExcludeElements); + return parser.encodeResourceToString(theResource); + } + + /** + * helper for returning the encoded byte array of the input resource string based on the theEncoding. + * + * @param theEncoding the theEncoding to used + * @param theEncodedResource the resource to encode + * @return byte array of the resource + */ + @Nonnull + static byte[] getResourceBinary(ResourceEncodingEnum theEncoding, String theEncodedResource) { + switch (theEncoding) { + case JSON: + return theEncodedResource.getBytes(StandardCharsets.UTF_8); + case JSONC: + return GZipUtil.compress(theEncodedResource); + default: + return new byte[0]; + } + } + + void populateEncodedResource( + EncodedResource theEncodedResource, + String theEncodedResourceString, + @Nullable byte[] theResourceBinary, + ResourceEncodingEnum theEncoding) { + if (myIsOracleDialect) { + populateEncodedResourceInner(theEncodedResource, null, theResourceBinary, theEncoding); + } else { + populateEncodedResourceInner(theEncodedResource, theEncodedResourceString, null, ResourceEncodingEnum.JSON); + } + } + + private void populateEncodedResourceInner( + EncodedResource encodedResource, + String encodedResourceString, + byte[] theResourceBinary, + ResourceEncodingEnum theEncoding) { + encodedResource.setResourceText(encodedResourceString); + encodedResource.setResourceBinary(theResourceBinary); + encodedResource.setEncoding(theEncoding); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceHistoryState.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceHistoryState.java new file mode 100644 index 00000000000..bdf5cdfa8ee --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceHistoryState.java @@ -0,0 +1,105 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.dao; + +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import com.google.common.hash.HashCode; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +/** + * POJO to contain the results of {@link ResourceHistoryCalculator#calculateResourceHistoryState(IBaseResource, ResourceEncodingEnum, List)} + */ +public class ResourceHistoryState { + @Nullable + private final String myResourceText; + + @Nullable + private final byte[] myResourceBinary; + + private final ResourceEncodingEnum myEncoding; + private final HashCode myHashCode; + + public ResourceHistoryState( + @Nullable String theResourceText, + @Nullable byte[] theResourceBinary, + ResourceEncodingEnum theEncoding, + HashCode theHashCode) { + myResourceText = theResourceText; + myResourceBinary = theResourceBinary; + myEncoding = theEncoding; + myHashCode = theHashCode; + } + + @Nullable + public String getResourceText() { + return myResourceText; + } + + @Nullable + public byte[] getResourceBinary() { + return myResourceBinary; + } + + public ResourceEncodingEnum getEncoding() { + return myEncoding; + } + + public HashCode getHashCode() { + return myHashCode; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) { + return true; + } + if (theO == null || getClass() != theO.getClass()) { + return false; + } + ResourceHistoryState that = (ResourceHistoryState) theO; + return Objects.equals(myResourceText, that.myResourceText) + && Arrays.equals(myResourceBinary, that.myResourceBinary) + && myEncoding == that.myEncoding + && Objects.equals(myHashCode, that.myHashCode); + } + + @Override + public int hashCode() { + int result = Objects.hash(myResourceText, myEncoding, myHashCode); + result = 31 * result + Arrays.hashCode(myResourceBinary); + return result; + } + + @Override + public String toString() { + return new StringJoiner(", ", ResourceHistoryState.class.getSimpleName() + "[", "]") + .add("myResourceText='" + myResourceText + "'") + .add("myResourceBinary=" + Arrays.toString(myResourceBinary)) + .add("myEncoding=" + myEncoding) + .add("myHashCode=" + myHashCode) + .toString(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index b568257fdaa..6c254ac71a8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.dao.expunge; 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.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity; @@ -135,10 +134,9 @@ public class ExpungeEverythingService implements IExpungeEverythingService { ourLog.info("BEGINNING GLOBAL $expunge"); Propagation propagation = Propagation.REQUIRES_NEW; - ReadPartitionIdRequestDetails details = - ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_EXPUNGE); RequestPartitionId requestPartitionId = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequest, details); + myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( + theRequest, ProviderConstants.OPERATION_EXPUNGE); deleteAll(theRequest, propagation, requestPartitionId, counter); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index b21c64dba93..93c0de299e3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -477,10 +477,19 @@ public class IdHelperService implements IIdHelperService { @Override public Optional translatePidIdToForcedIdWithCache(JpaPid theId) { - return myMemoryCacheService.get( - MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, - theId.getId(), - pid -> myResourceTableDao.findById(pid).map(ResourceTable::asTypedFhirResourceId)); + // do getIfPresent and then put to avoid doing I/O inside the cache. + Optional forcedId = + myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getId()); + + if (forcedId == null) { + // This is only called when we know the resource exists. + // So this optional is only empty when there is no hfj_forced_id table + // note: this is obsolete with the new fhir_id column, and will go away. + forcedId = myResourceTableDao.findById(theId.getId()).map(ResourceTable::asTypedFhirResourceId); + myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getId(), forcedId); + } + + return forcedId; } private ListMultimap organizeIdsByResourceType(Collection theIds) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 9d50199fb88..4046890a9f0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -133,10 +133,12 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { mdmLinkTable .addIndex("20230911.1", "IDX_EMPI_TGT_MR_LS") .unique(false) + .online(true) .withColumns("TARGET_TYPE", "MATCH_RESULT", "LINK_SOURCE"); mdmLinkTable .addIndex("20230911.2", "IDX_EMPi_TGT_MR_SCore") .unique(false) + .online(true) .withColumns("TARGET_TYPE", "MATCH_RESULT", "SCORE"); // Move forced_id constraints to hfj_resource and the new fhir_id column @@ -166,7 +168,11 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .withColumns("RES_TYPE", "FHIR_ID"); // For resolving references that don't supply the type. - hfjResource.addIndex("20231027.3", "IDX_RES_FHIR_ID").unique(false).withColumns("FHIR_ID"); + hfjResource + .addIndex("20231027.3", "IDX_RES_FHIR_ID") + .unique(false) + .online(true) + .withColumns("FHIR_ID"); Builder.BuilderWithTableName batch2JobInstanceTable = version.onTable("BT2_JOB_INSTANCE"); @@ -177,25 +183,32 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { { version.executeRawSql( "20231212.1", - "CREATE INDEX idx_sp_string_hash_nrm_pattern_ops ON public.hfj_spidx_string USING btree (hash_norm_prefix, sp_value_normalized varchar_pattern_ops, res_id, partition_id)") + "CREATE INDEX CONCURRENTLY idx_sp_string_hash_nrm_pattern_ops ON public.hfj_spidx_string USING btree (hash_norm_prefix, sp_value_normalized varchar_pattern_ops, res_id, partition_id)") + .setTransactional(false) .onlyAppliesToPlatforms(DriverTypeEnum.POSTGRES_9_4) .onlyIf( String.format( QUERY_FOR_COLUMN_COLLATION_TEMPLATE, "HFJ_SPIDX_STRING".toLowerCase(), "SP_VALUE_NORMALIZED".toLowerCase()), - "Column HFJ_SPIDX_STRING.SP_VALUE_NORMALIZED already has a collation of 'C' so doing nothing"); - + "Column HFJ_SPIDX_STRING.SP_VALUE_NORMALIZED already has a collation of 'C' so doing nothing") + .onlyIf( + "SELECT NOT EXISTS(select 1 from pg_indexes where indexname='idx_sp_string_hash_nrm_pattern_ops')", + "Index idx_sp_string_hash_nrm_pattern_ops already exists"); version.executeRawSql( "20231212.2", - "CREATE UNIQUE INDEX idx_sp_uri_hash_identity_pattern_ops ON public.hfj_spidx_uri USING btree (hash_identity, sp_uri varchar_pattern_ops, res_id, partition_id)") + "CREATE UNIQUE INDEX CONCURRENTLY idx_sp_uri_hash_identity_pattern_ops ON public.hfj_spidx_uri USING btree (hash_identity, sp_uri varchar_pattern_ops, res_id, partition_id)") + .setTransactional(false) .onlyAppliesToPlatforms(DriverTypeEnum.POSTGRES_9_4) .onlyIf( String.format( QUERY_FOR_COLUMN_COLLATION_TEMPLATE, "HFJ_SPIDX_URI".toLowerCase(), "SP_URI".toLowerCase()), - "Column HFJ_SPIDX_STRING.SP_VALUE_NORMALIZED already has a collation of 'C' so doing nothing"); + "Column HFJ_SPIDX_STRING.SP_VALUE_NORMALIZED already has a collation of 'C' so doing nothing") + .onlyIf( + "SELECT NOT EXISTS(select 1 from pg_indexes where indexname='idx_sp_uri_hash_identity_pattern_ops')", + "Index idx_sp_uri_hash_identity_pattern_ops already exists."); } // This fix was bad for MSSQL, it has been set to do nothing. @@ -622,6 +635,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { version.executeRawSqls("20230402.1", Map.of(DriverTypeEnum.POSTGRES_9_4, postgresTuningStatements)); // Use an unlimited length text column for RES_TEXT_VC + // N.B. This will FAIL SILENTLY on Oracle due to the fact that Oracle does not support an ALTER TABLE from + // VARCHAR to + // CLOB. Because of failureAllowed() this won't halt the migration version.onTable("HFJ_RES_VER") .modifyColumn("20230421.1", "RES_TEXT_VC") .nullable() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index 673e239ae8d..9cd22443e97 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -296,7 +296,8 @@ public class QueryStack { String theReferenceTargetType, String theParamName, String theChain, - boolean theAscending) { + boolean theAscending, + SearchParameterMap theParams) { BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this); @@ -378,13 +379,37 @@ public class QueryStack { * sort on a target that was a reference or a quantity, but if someone needed * that we could implement it here. */ + case SPECIAL: { + if (LOCATION_POSITION.equals(targetSearchParameter.getPath())) { + List> params = theParams.get(theParamName); + if (params != null && !params.isEmpty() && !params.get(0).isEmpty()) { + IQueryParameterType locationParam = params.get(0).get(0); + final SpecialParam specialParam = + new SpecialParam().setValue(locationParam.getValueAsQueryToken(myFhirContext)); + ParsedLocationParam location = ParsedLocationParam.from(theParams, specialParam); + double latitudeValue = location.getLatitudeValue(); + double longitudeValue = location.getLongitudeValue(); + final CoordsPredicateBuilder coordsPredicateBuilder = mySqlBuilder.addCoordsPredicateBuilder( + resourceLinkPredicateBuilder.getColumnTargetResourceId()); + mySqlBuilder.addSortCoordsNear( + coordsPredicateBuilder, latitudeValue, longitudeValue, theAscending); + } else { + String msg = myFhirContext + .getLocalizer() + .getMessageSanitized( + QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName); + throw new InvalidRequestException(Msg.code(2497) + msg); + } + return; + } + } case NUMBER: case REFERENCE: case COMPOSITE: case QUANTITY: case URI: case HAS: - case SPECIAL: + default: throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter " + theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: " @@ -2466,7 +2491,7 @@ public class QueryStack { theRequestPartitionId, andPredicates, nextAnd)) { - break; + continue; } EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index 3802048aef4..0874b2b41b5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -932,7 +932,7 @@ public class SearchBuilder implements ISearchBuilder { break; case REFERENCE: theQueryStack.addSortOnResourceLink( - myResourceName, referenceTargetType, paramName, chainName, ascending); + myResourceName, referenceTargetType, paramName, chainName, ascending, theParams); break; case TOKEN: theQueryStack.addSortOnToken(myResourceName, paramName, ascending); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java index 9a1243a5aaf..3848158b1d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java @@ -49,7 +49,6 @@ import ca.uhn.fhir.jpa.util.QueryParameterUtils; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -60,6 +59,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.healthmarketscience.sqlbuilder.BinaryCondition; import com.healthmarketscience.sqlbuilder.ComboCondition; @@ -70,6 +70,7 @@ import com.healthmarketscience.sqlbuilder.UnaryCondition; import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; @@ -83,16 +84,20 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.ListIterator; +import java.util.Optional; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with; +import static ca.uhn.fhir.rest.api.Constants.*; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.trim; public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder implements ICanMakeMissingParamPredicate { private static final Logger ourLog = LoggerFactory.getLogger(ResourceLinkPredicateBuilder.class); + private static final Pattern MODIFIER_REPLACE_PATTERN = Pattern.compile(".*:"); private final DbColumn myColumnSrcType; private final DbColumn myColumnSrcPath; private final DbColumn myColumnTargetResourceId; @@ -204,6 +209,8 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im targetQualifiedUrls.add(dt.getValue()); } } else { + validateModifierUse(theRequest, theResourceType, ref); + validateResourceTypeInReferenceParam(ref.getResourceType()); targetIds.add(dt); } @@ -256,6 +263,53 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im } } + private void validateModifierUse(RequestDetails theRequest, String theResourceType, ReferenceParam theRef) { + try { + final String resourceTypeFromRef = theRef.getResourceType(); + if (StringUtils.isEmpty(resourceTypeFromRef)) { + return; + } + // TODO: LD: unless we do this, ResourceProviderR4Test#testSearchWithSlashes will fail due to its + // derived-from: syntax + getFhirContext().getResourceDefinition(resourceTypeFromRef); + } catch (DataFormatException e) { + final List nonMatching = Optional.ofNullable(theRequest) + .map(RequestDetails::getParameters) + .map(params -> params.keySet().stream() + .filter(mod -> mod.contains(":")) + .map(MODIFIER_REPLACE_PATTERN::matcher) + .map(pattern -> pattern.replaceAll(":")) + .filter(mod -> !VALID_MODIFIERS.contains(mod)) + .distinct() + .collect(Collectors.toUnmodifiableList())) + .orElse(Collections.emptyList()); + + if (!nonMatching.isEmpty()) { + final String msg = getFhirContext() + .getLocalizer() + .getMessageSanitized( + SearchCoordinatorSvcImpl.class, + "invalidUseOfSearchIdentifier", + nonMatching, + theResourceType, + VALID_MODIFIERS); + throw new InvalidRequestException(Msg.code(2498) + msg); + } + } + } + + private void validateResourceTypeInReferenceParam(final String theResourceType) { + if (StringUtils.isEmpty(theResourceType)) { + return; + } + + try { + getFhirContext().getResourceDefinition(theResourceType); + } catch (DataFormatException e) { + throw newInvalidResourceTypeException(theResourceType); + } + } + private Condition createPredicateReference( boolean theInverse, List thePathsToMatch, @@ -355,18 +409,14 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im /* * Handle chain on _type */ - if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) { + if (PARAM_TYPE.equals(theReferenceParam.getChain())) { List pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers); Condition typeCondition = createPredicateSourcePaths(pathsToMatch); String typeValue = theReferenceParam.getValue(); - try { - getFhirContext().getResourceDefinition(typeValue).getImplementingClass(); - } catch (DataFormatException e) { - throw newInvalidResourceTypeException(typeValue); - } + validateResourceTypeInReferenceParam(typeValue); if (!resourceTypes.contains(typeValue)) { throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue); } @@ -705,7 +755,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im .getLocalizer() .getMessage( ResourceLinkPredicateBuilder.class, "invalidTargetTypeForChain", theTypeValue, searchParamName); - return new InvalidRequestException(msg); + return new InvalidRequestException(Msg.code(2495) + msg); } @Nonnull @@ -765,4 +815,14 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im return combineWithRequestPartitionIdPredicate(theParams.getRequestPartitionId(), unaryCondition); } + + @VisibleForTesting + void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) { + mySearchParamRegistry = theSearchParamRegistry; + } + + @VisibleForTesting + void setIdHelperServiceForUnitTest(IIdHelperService theIdHelperService) { + myIdHelperService = theIdHelperService; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImpl.java index ead7fc9bbc6..c7222458073 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/InstanceReindexServiceImpl.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.search.reindex; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.interceptor.api.IInterceptorService; -import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; @@ -33,6 +32,9 @@ import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.dao.JpaPid; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity; import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.partition.BaseRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; @@ -236,8 +238,7 @@ public class InstanceReindexServiceImpl implements IInstanceReindexService { @Nonnull private RequestPartitionId determinePartition(RequestDetails theRequestDetails, IIdType theResourceId) { - ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forRead(theResourceId); - return myPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details); + return myPartitionHelperSvc.determineReadPartitionForRequestForRead(theRequestDetails, theResourceId); } @Nonnull diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java index af712a2e197..86235e0403b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java @@ -649,7 +649,13 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { .getMessage(TermReadSvcImpl.class, "valueSetExpandedUsingPreExpansion", expansionTimestamp); theAccumulator.addMessage(msg); expandConcepts( - theExpansionOptions, theAccumulator, termValueSet, theFilter, theAdd, theAddedCodes, isOracleDialect()); + theExpansionOptions, + theAccumulator, + termValueSet, + theFilter, + theAdd, + theAddedCodes, + myHibernatePropertiesProvider.isOracleDialect()); } @Nonnull @@ -664,10 +670,6 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { return expansionTimestamp; } - private boolean isOracleDialect() { - return myHibernatePropertiesProvider.getDialect() instanceof org.hibernate.dialect.OracleDialect; - } - private void expandConcepts( ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theAccumulator, @@ -1596,6 +1598,16 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { TermConcept code = findCodeForFilterCriteria(theSystem, theFilter); if (theFilter.getOp() == ValueSet.FilterOperator.ISA) { + ourLog.debug( + " * Filtering on specific code and codes with a parent of {}/{}/{}", + code.getId(), + code.getCode(), + code.getDisplay()); + + b.must(f.bool() + .should(f.match().field("myParentPids").matching("" + code.getId())) + .should(f.match().field("myId").matching(code.getId()))); + } else if (theFilter.getOp() == ValueSet.FilterOperator.DESCENDENTOF) { ourLog.debug( " * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/ResourceHistoryCalculatorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/ResourceHistoryCalculatorTest.java new file mode 100644 index 00000000000..d2123cacbb5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/ResourceHistoryCalculatorTest.java @@ -0,0 +1,326 @@ +package ca.uhn.fhir.jpa.dao; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import org.hl7.fhir.dstu3.hapi.ctx.FhirDstu3; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.hapi.ctx.FhirR4; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.Month; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResourceHistoryCalculatorTest { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceHistoryCalculatorTest.class); + + private static final FhirContext CONTEXT = FhirContext.forR4Cached(); + + private static final ResourceHistoryCalculator CALCULATOR_ORACLE = new ResourceHistoryCalculator(CONTEXT, true); + private static final ResourceHistoryCalculator CALCULATOR_NON_ORACLE = new ResourceHistoryCalculator(CONTEXT, false); + + private static final LocalDate TODAY = LocalDate.of(2024, Month.JANUARY, 25); + private static final String ENCODED_RESOURCE_1 = "1234"; + private static final String ENCODED_RESOURCE_2 = "abcd"; + private static final String RESOURCE_TEXT_VC = "resourceTextVc"; + private static final List EXCLUDED_ELEMENTS_1 = List.of("id"); + private static final List EXCLUDED_ELEMENTS_2 = List.of("resourceType", "birthDate"); + private static final HashFunction SHA_256 = Hashing.sha256(); + + private static Stream calculateResourceHistoryStateArguments() { + return Stream.of( + Arguments.of(FhirContext.forDstu3Cached(), true, ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), false, ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), true, ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), false, ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), true, ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), false, ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), true, ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), false, ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), true, ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), false, ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), true, ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), false, ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), true, ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), false, ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), true, ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), false, ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_1) + ); + } + + /** + * The purpose of this test is to ensure that the conditional logic to pre-calculate resource history text or binaries + * is respected. + * If this is for Oracle, the resource text will be driven off a binary with a given encoding with the + * resource text effectively ignored. + * If this is not Oracle, it will be driven off a JSON encoded text field with + * the binary effectively ignored. + */ + @ParameterizedTest + @MethodSource("calculateResourceHistoryStateArguments") + void calculateResourceHistoryState(FhirContext theFhirContext, boolean theIsOracle, ResourceEncodingEnum theResourceEncoding, List theExcludedElements) { + final IBaseResource patient = getPatient(theFhirContext); + + final ResourceHistoryCalculator calculator = getCalculator(theFhirContext, theIsOracle); + final ResourceHistoryState result = calculator.calculateResourceHistoryState(patient, theResourceEncoding, theExcludedElements); + + if (theIsOracle) { + assertNotNull(result.getResourceBinary()); // On Oracle: We use the resource binary to serve up the resource content + assertNull(result.getResourceText()); // On Oracle: We do NOT use the resource text to serve up the resource content + assertEquals(theResourceEncoding, result.getEncoding()); // On Oracle, the resource encoding is what we used to encode the binary + assertEquals(SHA_256.hashBytes(result.getResourceBinary()), result.getHashCode()); // On Oracle, the SHA 256 hash is of the binary + } else { + assertNull(result.getResourceBinary()); // Non-Oracle: We do NOT use the resource binary to serve up the resource content + assertNotNull(result.getResourceText()); // Non-Oracle: We use the resource text to serve up the resource content + assertEquals(ResourceEncodingEnum.JSON, result.getEncoding()); // Non-Oracle, since we didn't encode a binary this is always JSON. + final HashCode expectedHashCode = SHA_256.hashUnencodedChars(calculator.encodeResource(patient, theResourceEncoding, theExcludedElements)); // Non-Oracle, the SHA 256 hash is of the parsed resource object + assertEquals(expectedHashCode, result.getHashCode()); + } + } + + + private static Stream conditionallyAlterHistoryEntityArguments() { + return Stream.of( + Arguments.of(true, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_2) + ); + } + + @ParameterizedTest + @MethodSource("conditionallyAlterHistoryEntityArguments") + void conditionallyAlterHistoryEntity_usesVarcharForOracle(boolean theIsOracle, ResourceEncodingEnum theResourceEncoding, String theResourceText) { + final ResourceTable resourceTable = new ResourceTable(); + resourceTable.setId(123L); + + final ResourceHistoryTable resourceHistoryTable = new ResourceHistoryTable(); + resourceHistoryTable.setVersion(1); + resourceHistoryTable.setResource("resource".getBytes(StandardCharsets.UTF_8)); + resourceHistoryTable.setEncoding(theResourceEncoding); + resourceHistoryTable.setResourceTextVc(RESOURCE_TEXT_VC); + + final boolean isChanged = + getCalculator(theIsOracle).conditionallyAlterHistoryEntity(resourceTable, resourceHistoryTable, theResourceText); + + if (theIsOracle) { + assertFalse(isChanged); + assertNotNull(resourceHistoryTable.getResource()); + assertEquals(RESOURCE_TEXT_VC, resourceHistoryTable.getResourceTextVc()); + assertEquals(resourceHistoryTable.getEncoding(), resourceHistoryTable.getEncoding()); + } else { + assertTrue(isChanged); + assertNull(resourceHistoryTable.getResource()); + assertEquals(theResourceText, resourceHistoryTable.getResourceTextVc()); + assertEquals(resourceHistoryTable.getEncoding(), ResourceEncodingEnum.JSON); + } + } + + private static Stream encodeResourceArguments() { + return Stream.of( + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forDstu3Cached(), ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.JSONC, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.DEL, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.ESR, EXCLUDED_ELEMENTS_2), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_1), + Arguments.of(FhirContext.forR4Cached(), ResourceEncodingEnum.JSON, EXCLUDED_ELEMENTS_2) + ); + } + + @ParameterizedTest + @MethodSource("encodeResourceArguments") + void encodeResource_ensureFhirVersionSpecificAndIntendedElementsExcluded(FhirContext theFhirContext, ResourceEncodingEnum theResourceEncoding, List theExcludedElements) { + final IBaseResource patient = getPatient(theFhirContext); + final String encodedResource = getCalculator(theFhirContext, true).encodeResource(patient, theResourceEncoding, theExcludedElements); + + final String expectedEncoding = + theResourceEncoding.newParser(theFhirContext).setDontEncodeElements(theExcludedElements).encodeResourceToString(patient); + + assertEquals(expectedEncoding, encodedResource); + } + + private static Stream getResourceBinaryArguments() { + return Stream.of( + Arguments.of(ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_1), + Arguments.of(ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_2), + Arguments.of(ResourceEncodingEnum.DEL, ENCODED_RESOURCE_1), + Arguments.of(ResourceEncodingEnum.DEL, ENCODED_RESOURCE_2), + Arguments.of(ResourceEncodingEnum.ESR, ENCODED_RESOURCE_1), + Arguments.of(ResourceEncodingEnum.ESR, ENCODED_RESOURCE_2), + Arguments.of(ResourceEncodingEnum.JSON, ENCODED_RESOURCE_1), + Arguments.of(ResourceEncodingEnum.JSON, ENCODED_RESOURCE_2) + ); + } + + @ParameterizedTest + @MethodSource("getResourceBinaryArguments") + void getResourceBinary(ResourceEncodingEnum theResourceEncoding, String theEncodedResource) { + final byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(theResourceEncoding, theEncodedResource); + + switch (theResourceEncoding) { + case JSON: + assertArrayEquals(theEncodedResource.getBytes(StandardCharsets.UTF_8), resourceBinary); + break; + case JSONC: + assertArrayEquals(GZipUtil.compress(theEncodedResource), resourceBinary); + break; + case DEL : + case ESR : + default: + assertArrayEquals(new byte[0], resourceBinary); + } + + ourLog.info("resourceBinary: {}", resourceBinary); + } + + private static Stream isResourceHistoryChangedArguments() { + return Stream.of( + Arguments.of(true, ENCODED_RESOURCE_1.getBytes(StandardCharsets.UTF_8), ENCODED_RESOURCE_1), + Arguments.of(false, ENCODED_RESOURCE_1.getBytes(StandardCharsets.UTF_8), ENCODED_RESOURCE_1), + Arguments.of(true, ENCODED_RESOURCE_2.getBytes(StandardCharsets.UTF_8), ENCODED_RESOURCE_2), + Arguments.of(false, ENCODED_RESOURCE_2.getBytes(StandardCharsets.UTF_8), ENCODED_RESOURCE_2) + ); + } + + @ParameterizedTest + @MethodSource("isResourceHistoryChangedArguments") + void isResourceHistoryChanged(boolean theIsOracle, byte[] theNewBinary, String theNewResourceText) { + final String existngResourceText = ENCODED_RESOURCE_1; + final byte[] existingBytes = existngResourceText.getBytes(StandardCharsets.UTF_8); + + final ResourceHistoryTable resourceHistoryTable = new ResourceHistoryTable(); + resourceHistoryTable.setResource(existingBytes); + resourceHistoryTable.setResourceTextVc(existngResourceText); + + final boolean isChanged = getCalculator(theIsOracle).isResourceHistoryChanged(resourceHistoryTable, theNewBinary, theNewResourceText); + + if (theIsOracle) { + final boolean expectedResult = !Arrays.equals(existingBytes, theNewBinary); + assertEquals(expectedResult, isChanged); + } else { + final boolean expectedResult = ! existngResourceText.equals(theNewResourceText); + assertEquals(expectedResult, isChanged); + } + } + + private static Stream populateEncodedResourceArguments() { + return Stream.of( + Arguments.of(true, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.JSONC, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.DEL, ENCODED_RESOURCE_1), + Arguments.of(true, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_1), + Arguments.of(false, ResourceEncodingEnum.ESR, ENCODED_RESOURCE_2), + Arguments.of(true, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_2), + Arguments.of(false, ResourceEncodingEnum.JSON, ENCODED_RESOURCE_1) + ); + } + + @ParameterizedTest + @MethodSource("populateEncodedResourceArguments") + void populateEncodedResource(boolean theIsOracle, ResourceEncodingEnum theResourceEncoding, String theEncodedResourceString) { + final EncodedResource encodedResource = new EncodedResource(); + final byte[] resourceBinary = theEncodedResourceString.getBytes(StandardCharsets.UTF_8); + + getCalculator(theIsOracle) + .populateEncodedResource(encodedResource, theEncodedResourceString, resourceBinary, theResourceEncoding); + + if (theIsOracle) { + assertEquals(resourceBinary, encodedResource.getResourceBinary()); + assertNull(encodedResource.getResourceText()); + assertEquals(theResourceEncoding, encodedResource.getEncoding()); + } else { + assertNull(encodedResource.getResourceBinary()); + assertEquals(theEncodedResourceString, encodedResource.getResourceText()); + assertEquals(ResourceEncodingEnum.JSON, encodedResource.getEncoding()); + } + } + + private ResourceHistoryCalculator getCalculator(boolean theIsOracle) { + return theIsOracle ? CALCULATOR_ORACLE : CALCULATOR_NON_ORACLE; + } + + private ResourceHistoryCalculator getCalculator(FhirContext theFhirContext, boolean theIsOracle) { + return new ResourceHistoryCalculator(theFhirContext, theIsOracle); + } + + private IBaseResource getPatient(FhirContext theFhirContext) { + if (theFhirContext.getVersion() instanceof FhirR4) { + return getPatientR4(); + } + + if (theFhirContext.getVersion() instanceof FhirDstu3) { + return getPatientDstu3(); + } + + return null; + } + + private org.hl7.fhir.dstu3.model.Patient getPatientDstu3() { + final org.hl7.fhir.dstu3.model.Patient patient = new org.hl7.fhir.dstu3.model.Patient(); + + patient.setId("123"); + patient.setBirthDate(Date.from(TODAY.atStartOfDay(ZoneId.of("America/Toronto")).toInstant())); + + return patient; + } + + private Patient getPatientR4() { + final Patient patient = new Patient(); + + patient.setId("123"); + patient.setBirthDate(Date.from(TODAY.atStartOfDay(ZoneId.of("America/Toronto")).toInstant())); + + return patient; + } +} diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 1535dc6758a..f398397c578 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java index a17f61fe13e..615f5d9e165 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java @@ -786,7 +786,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl logAndValidateValueSet(result); ArrayList codes = toCodesContains(result.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("childAAA", "childAAB")); + assertThat(codes, containsInAnyOrder("childAA", "childAAA", "childAAB")); } @Test diff --git a/hapi-fhir-jpaserver-hfql/pom.xml b/hapi-fhir-jpaserver-hfql/pom.xml index 8adf95cafcd..cf1df93b100 100644 --- a/hapi-fhir-jpaserver-hfql/pom.xml +++ b/hapi-fhir-jpaserver-hfql/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml index a650708ac11..9725938e05c 100644 --- a/hapi-fhir-jpaserver-ips/pom.xml +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java index 8fd6f4a5554..bad1470d646 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java @@ -19,15 +19,17 @@ */ package ca.uhn.fhir.jpa.ips.api; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.jpa.ips.strategy.BaseIpsGenerationStrategy; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import java.util.List; -import java.util.Set; /** * This interface is the primary configuration and strategy provider for the @@ -39,11 +41,34 @@ import java.util.Set; public interface IIpsGenerationStrategy { /** - * Provides a registry which defines the various sections that will be - * included when generating an IPS. It can be subclassed and customized - * as needed in order to add, change, or remove sections. + * This method returns the profile associated with the IPS document + * generated by this strategy. */ - SectionRegistry getSectionRegistry(); + String getBundleProfile(); + + /** + * This method will be called once by the framework. It can be + * used to perform any initialization. + */ + void initialize(); + + /** + * This method should return a list of the sections to include in the + * generated IPS. Note that each section must have a unique value for the + * {@link Section#getProfile()} value. + */ + @Nonnull + List
getSections(); + + /** + * Returns the resource supplier for the given section. The resource supplier + * is used to supply the resources which will be used for a given + * section. + * + * @param theSection The section + */ + @Nonnull + ISectionResourceSupplier getSectionResourceSupplier(@Nonnull Section theSection); /** * Provides a list of configuration property files for the IPS narrative generator. @@ -53,7 +78,7 @@ public interface IIpsGenerationStrategy { *

* If more than one file is provided, the files will be evaluated in order. Therefore you * might choose to include a custom file, followed by - * {@link ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy#DEFAULT_IPS_NARRATIVES_PROPERTIES} + * {@link BaseIpsGenerationStrategy#DEFAULT_IPS_NARRATIVES_PROPERTIES} * in order to fall back to the default templates for any sections you have not * provided an explicit template for. *

@@ -85,7 +110,13 @@ public interface IIpsGenerationStrategy { /** * This method is used to determine the resource ID to assign to a resource that * will be added to the IPS document Bundle. Implementations will probably either - * return the resource ID as-is, or generate a placeholder UUID to replace it with. + * return null to leave the resource ID as-is, or generate a + * placeholder UUID to replace it with. + *

+ * If you want to replace the native resource ID with a placeholder so as not + * to leak the server-generated IDs, the recommended way is to + * return IdType.newRandomUuid() + *

* * @param theIpsContext The associated context for the specific IPS document being * generated. Note that this will be null when @@ -93,43 +124,33 @@ public interface IIpsGenerationStrategy { * be populated for all subsequent calls for a given IPS * document generation. * @param theResource The resource to massage the resource ID for - * @return An ID to assign to the resource + * @return An ID to assign to the resource, or null to leave the existing ID intact, + * meaning that the server-assigned IDs will be used in the bundle. */ + @Nullable IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource); /** - * This method can manipulate the {@link SearchParameterMap} that will - * be used to find candidate resources for the given IPS section. The map will already have - * a subject/patient parameter added to it. The map provided in {@literal theSearchParameterMap} - * will contain a subject/patient reference, but no other parameters. This method can add other - * parameters. - *

- * For example, for a Vital Signs section, the implementation might add a parameter indicating - * the parameter category=vital-signs. + * Fetches and returns the patient to include in the generated IPS for the given patient ID. * - * @param theIpsSectionContext The context, which indicates the IPS section and the resource type - * being searched for. - * @param theSearchParameterMap The map to manipulate. - */ - void massageResourceSearch( - IpsContext.IpsSectionContext theIpsSectionContext, SearchParameterMap theSearchParameterMap); - - /** - * Return a set of Include directives to be added to the resource search - * for resources to include for a given IPS section. These include statements will - * be added to the same {@link SearchParameterMap} provided to - * {@link #massageResourceSearch(IpsContext.IpsSectionContext, SearchParameterMap)}. - * This is a separate method in order to make subclassing easier. - * - * @param theIpsSectionContext The context, which indicates the IPS section and the resource type - * being searched for. + * @throws ResourceNotFoundException If the ID is not known. */ @Nonnull - Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext); + IBaseResource fetchPatient(IIdType thePatientId, RequestDetails theRequestDetails) throws ResourceNotFoundException; /** - * This method will be called for each found resource candidate for inclusion in the - * IPS document. The strategy can decide whether to include it or not. + * Fetches and returns the patient to include in the generated IPS for the given patient identifier. + * + * @throws ResourceNotFoundException If the ID is not known. */ - boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate); + @Nonnull + IBaseResource fetchPatient(TokenParam thePatientIdentifier, RequestDetails theRequestDetails); + + /** + * This method is called once for each generated IPS document, after all other processing is complete. It can + * be used by the strategy to make direct manipulations prior to returning the document. + */ + default void postManipulateIpsBundle(IBaseBundle theBundle) { + // nothing + } } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java new file mode 100644 index 00000000000..cbb1c18375d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java @@ -0,0 +1,39 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.api; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +/** + * This interface is invoked when a section has no resources found, and should generate + * a "stub" resource explaining why. Typically this would be content such as "no information + * is available for this section", and might indicate for example that the absence of + * AllergyIntolerance resources only indicates that the allergy status is not known, not that + * the patient has no allergies. + */ +public interface INoInfoGenerator { + + /** + * Generate an appropriate no-info resource. The resource does not need to have an ID populated, + * although it can if it is a resource found in the repository. + */ + IBaseResource generate(IIdType theSubjectId); +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java new file mode 100644 index 00000000000..63543eb749d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java @@ -0,0 +1,125 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.api; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.thymeleaf.util.Validate; + +import java.util.List; + +/** + * This interface is invoked for each section of the IPS, and fetches/returns the + * resources which will be included in the IPS document for that section. This + * might be by performing a search in a local repository, but could also be + * done by calling a remote repository, performing a calculation, making + * JDBC database calls directly, etc. + *

+ * Note that you only need to implement this interface directly if you want to + * provide manual logic for gathering and preparing resources to include in + * an IPS document. If your resources can be collected by querying a JPS + * repository, you can use {@link ca.uhn.fhir.jpa.ips.jpa.JpaSectionResourceSupplier} + * as the implementation of this interface, and + * {@link ca.uhn.fhir.jpa.ips.jpa.IJpaSectionSearchStrategy} becomes the class + * that is used to define your searches. + *

+ * + * @since 7.2.0 + * @see ca.uhn.fhir.jpa.ips.jpa.JpaSectionResourceSupplier + */ +public interface ISectionResourceSupplier { + + /** + * This method will be called once for each section context (section and resource type combination), + * and will be used to supply the resources to include in the given IPS section. This method can + * be used if you wish to fetch resources for a given section from a source other than + * the repository. This could mean fetching resources using a FHIR REST client to an + * external server, or could even mean fetching data directly from a database using JDBC + * or similar. + * + * @param theIpsContext The IPS context, containing the identity of the patient whose IPS is being generated. + * @param theSectionContext The section context, containing the section name and resource type. + * @param theRequestDetails The RequestDetails object associated with the HTTP request associated with this generation. + * @return Returns a list of resources to add to the given section, or null. + */ + @Nullable + List fetchResourcesForSection( + IpsContext theIpsContext, IpsSectionContext theSectionContext, RequestDetails theRequestDetails); + + /** + * This enum specifies how an individual {@link ResourceEntry resource entry} that + * is returned by {@link #fetchResourcesForSection(IpsContext, IpsSectionContext, RequestDetails)} + * should be included in the resulting IPS document bundle. + */ + enum InclusionTypeEnum { + + /** + * The resource should be included in the document bundle and linked to + * from the Composition via the Composition.section.entry + * reference. + */ + PRIMARY_RESOURCE, + + /** + * The resource should be included in the document bundle, but not directly + * linked from the composition. This typically means that it is referenced + * by at least one primary resource. + */ + SECONDARY_RESOURCE, + + /** + * Do not include this resource in the document + */ + EXCLUDE + } + + /** + * This class is the return type for {@link #fetchResourcesForSection(IpsContext, IpsSectionContext, RequestDetails)}. + */ + class ResourceEntry { + + private final IBaseResource myResource; + + private final InclusionTypeEnum myInclusionType; + + /** + * Constructor + * + * @param theResource The resource to include (must not be null) + * @param theInclusionType The inclusion type (must not be null) + */ + public ResourceEntry(@Nonnull IBaseResource theResource, @Nonnull InclusionTypeEnum theInclusionType) { + Validate.notNull(theResource, "theResource must not be null"); + Validate.notNull(theInclusionType, "theInclusionType must not be null"); + myResource = theResource; + myInclusionType = theInclusionType; + } + + public IBaseResource getResource() { + return myResource; + } + + public InclusionTypeEnum getInclusionType() { + return myInclusionType; + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java index 5424d8791cc..f57d8c381ec 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java @@ -58,28 +58,8 @@ public class IpsContext { return mySubjectId; } - public IpsSectionContext newSectionContext(IpsSectionEnum theSection, String theResourceType) { - return new IpsSectionContext(mySubject, mySubjectId, theSection, theResourceType); - } - - public static class IpsSectionContext extends IpsContext { - - private final IpsSectionEnum mySection; - private final String myResourceType; - - private IpsSectionContext( - IBaseResource theSubject, IIdType theSubjectId, IpsSectionEnum theSection, String theResourceType) { - super(theSubject, theSubjectId); - mySection = theSection; - myResourceType = theResourceType; - } - - public String getResourceType() { - return myResourceType; - } - - public IpsSectionEnum getSection() { - return mySection; - } + public IpsSectionContext newSectionContext( + Section theSection, Class theResourceType) { + return new IpsSectionContext<>(mySubject, mySubjectId, theSection, theResourceType); } } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java new file mode 100644 index 00000000000..669c8c5d389 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java @@ -0,0 +1,43 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.api; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +public class IpsSectionContext extends IpsContext { + + private final Section mySection; + private final Class myResourceType; + + IpsSectionContext(IBaseResource theSubject, IIdType theSubjectId, Section theSection, Class theResourceType) { + super(theSubject, theSubjectId); + mySection = theSection; + myResourceType = theResourceType; + } + + public Class getResourceType() { + return myResourceType; + } + + public Section getSection() { + return mySection; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java new file mode 100644 index 00000000000..7dfb47fcfc2 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java @@ -0,0 +1,223 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.api; + +import jakarta.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.ArrayList; +import java.util.List; + +/** + * Call {@link #newBuilder()} to create a new instance of this class. + */ +public class Section { + + private final String myTitle; + private final String mySectionCode; + private final String mySectionDisplay; + private final List> myResourceTypes; + private final String myProfile; + private final INoInfoGenerator myNoInfoGenerator; + + private final String mySectionSystem; + + private Section( + String theTitle, + String theSectionSystem, + String theSectionCode, + String theSectionDisplay, + List> theResourceTypes, + String theProfile, + INoInfoGenerator theNoInfoGenerator) { + myTitle = theTitle; + mySectionSystem = theSectionSystem; + mySectionCode = theSectionCode; + mySectionDisplay = theSectionDisplay; + myResourceTypes = List.copyOf(theResourceTypes); + myProfile = theProfile; + myNoInfoGenerator = theNoInfoGenerator; + } + + @Nullable + public INoInfoGenerator getNoInfoGenerator() { + return myNoInfoGenerator; + } + + public List> getResourceTypes() { + return myResourceTypes; + } + + public String getProfile() { + return myProfile; + } + + public String getTitle() { + return myTitle; + } + + public String getSectionSystem() { + return mySectionSystem; + } + + public String getSectionCode() { + return mySectionCode; + } + + public String getSectionDisplay() { + return mySectionDisplay; + } + + @Override + public boolean equals(Object theO) { + if (theO instanceof Section) { + Section o = (Section) theO; + return StringUtils.equals(myProfile, o.myProfile); + } + return false; + } + + @Override + public int hashCode() { + return myProfile.hashCode(); + } + + /** + * Create a new empty section builder + */ + public static SectionBuilder newBuilder() { + return new SectionBuilder(); + } + + /** + * Create a new section builder which is a clone of an existing section + */ + public static SectionBuilder newBuilder(Section theSection) { + return new SectionBuilder( + theSection.myTitle, + theSection.mySectionSystem, + theSection.mySectionCode, + theSection.mySectionDisplay, + theSection.myProfile, + theSection.myNoInfoGenerator, + theSection.myResourceTypes); + } + + public static class SectionBuilder { + + private String myTitle; + private String mySectionSystem; + private String mySectionCode; + private String mySectionDisplay; + private List> myResourceTypes = new ArrayList<>(); + private String myProfile; + private INoInfoGenerator myNoInfoGenerator; + + private SectionBuilder() { + super(); + } + + public SectionBuilder( + String theTitle, + String theSectionSystem, + String theSectionCode, + String theSectionDisplay, + String theProfile, + INoInfoGenerator theNoInfoGenerator, + List> theResourceTypes) { + myTitle = theTitle; + mySectionSystem = theSectionSystem; + mySectionCode = theSectionCode; + mySectionDisplay = theSectionDisplay; + myNoInfoGenerator = theNoInfoGenerator; + myProfile = theProfile; + myResourceTypes = new ArrayList<>(theResourceTypes); + } + + public SectionBuilder withTitle(String theTitle) { + Validate.notBlank(theTitle); + myTitle = theTitle; + return this; + } + + public SectionBuilder withSectionSystem(String theSectionSystem) { + Validate.notBlank(theSectionSystem); + mySectionSystem = theSectionSystem; + return this; + } + + public SectionBuilder withSectionCode(String theSectionCode) { + Validate.notBlank(theSectionCode); + mySectionCode = theSectionCode; + return this; + } + + public SectionBuilder withSectionDisplay(String theSectionDisplay) { + Validate.notBlank(theSectionDisplay); + mySectionDisplay = theSectionDisplay; + return this; + } + + /** + * This method may be called multiple times if the section will contain multiple resource types + */ + public SectionBuilder withResourceType(Class theResourceType) { + Validate.notNull(theResourceType, "theResourceType must not be null"); + Validate.isTrue(!myResourceTypes.contains(theResourceType), "theResourceType has already been added"); + myResourceTypes.add(theResourceType); + return this; + } + + public SectionBuilder withProfile(String theProfile) { + Validate.notBlank(theProfile); + myProfile = theProfile; + return this; + } + + /** + * Supplies a {@link INoInfoGenerator} which is used to create a stub resource + * to place in this section if no actual contents are found. This can be + * {@literal null} if you do not want any such stub to be included for this + * section. + */ + @SuppressWarnings("UnusedReturnValue") + public SectionBuilder withNoInfoGenerator(@Nullable INoInfoGenerator theNoInfoGenerator) { + myNoInfoGenerator = theNoInfoGenerator; + return this; + } + + public Section build() { + Validate.notBlank(mySectionSystem, "No section system has been defined for this section"); + Validate.notBlank(mySectionCode, "No section code has been defined for this section"); + Validate.notBlank(mySectionDisplay, "No section display has been defined for this section"); + + return new Section( + myTitle, + mySectionSystem, + mySectionCode, + mySectionDisplay, + myResourceTypes, + myProfile, + myNoInfoGenerator); + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java deleted file mode 100644 index e7940638d78..00000000000 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java +++ /dev/null @@ -1,470 +0,0 @@ -/*- - * #%L - * HAPI FHIR JPA Server - International Patient Summary (IPS) - * %% - * Copyright (C) 2014 - 2024 Smile CDR, Inc. - * %% - * 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% - */ -package ca.uhn.fhir.jpa.ips.api; - -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.AllergyIntolerance; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Condition; -import org.hl7.fhir.r4.model.MedicationStatement; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.ResourceType; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -/** - * This class is the registry for sections for the IPS document. It can be extended - * and customized if you wish to add / remove / change sections. - *

- * By default, all standard sections in the - * base IPS specification IG - * are included. You can customize this to remove sections, or to add new ones - * as permitted by the IG. - *

- *

- * To customize the sections, you may override the {@link #addSections()} method - * in order to add new sections or remove them. You may also override individual - * section methods such as {@link #addSectionAllergyIntolerance()} or - * {@link #addSectionAdvanceDirectives()}. - *

- */ -public class SectionRegistry { - - private final ArrayList
mySections = new ArrayList<>(); - private List> myGlobalCustomizers = new ArrayList<>(); - - /** - * Constructor - */ - public SectionRegistry() { - super(); - } - - /** - * This method should be automatically called by the Spring context. It initializes - * the registry. - */ - @PostConstruct - public final void initialize() { - Validate.isTrue(mySections.isEmpty(), "Sections are already initialized"); - addSections(); - } - - public boolean isInitialized() { - return !mySections.isEmpty(); - } - - /** - * Add the various sections to the registry in order. This method can be overridden for - * customization. - */ - protected void addSections() { - addSectionAllergyIntolerance(); - addSectionMedicationSummary(); - addSectionProblemList(); - addSectionImmunizations(); - addSectionProcedures(); - addSectionMedicalDevices(); - addSectionDiagnosticResults(); - addSectionVitalSigns(); - addSectionPregnancy(); - addSectionSocialHistory(); - addSectionIllnessHistory(); - addSectionFunctionalStatus(); - addSectionPlanOfCare(); - addSectionAdvanceDirectives(); - } - - protected void addSectionAllergyIntolerance() { - addSection(IpsSectionEnum.ALLERGY_INTOLERANCE) - .withTitle("Allergies and Intolerances") - .withSectionCode("48765-2") - .withSectionDisplay("Allergies and adverse reactions Document") - .withResourceTypes(ResourceType.AllergyIntolerance.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies") - .withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator()) - .build(); - } - - protected void addSectionMedicationSummary() { - addSection(IpsSectionEnum.MEDICATION_SUMMARY) - .withTitle("Medication List") - .withSectionCode("10160-0") - .withSectionDisplay("History of Medication use Narrative") - .withResourceTypes( - ResourceType.MedicationStatement.name(), - ResourceType.MedicationRequest.name(), - ResourceType.MedicationAdministration.name(), - ResourceType.MedicationDispense.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications") - .withNoInfoGenerator(new MedicationNoInfoR4Generator()) - .build(); - } - - protected void addSectionProblemList() { - addSection(IpsSectionEnum.PROBLEM_LIST) - .withTitle("Problem List") - .withSectionCode("11450-4") - .withSectionDisplay("Problem list - Reported") - .withResourceTypes(ResourceType.Condition.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems") - .withNoInfoGenerator(new ProblemNoInfoR4Generator()) - .build(); - } - - protected void addSectionImmunizations() { - addSection(IpsSectionEnum.IMMUNIZATIONS) - .withTitle("History of Immunizations") - .withSectionCode("11369-6") - .withSectionDisplay("History of Immunization Narrative") - .withResourceTypes(ResourceType.Immunization.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations") - .build(); - } - - protected void addSectionProcedures() { - addSection(IpsSectionEnum.PROCEDURES) - .withTitle("History of Procedures") - .withSectionCode("47519-4") - .withSectionDisplay("History of Procedures Document") - .withResourceTypes(ResourceType.Procedure.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx") - .build(); - } - - protected void addSectionMedicalDevices() { - addSection(IpsSectionEnum.MEDICAL_DEVICES) - .withTitle("Medical Devices") - .withSectionCode("46264-8") - .withSectionDisplay("History of medical device use") - .withResourceTypes(ResourceType.DeviceUseStatement.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices") - .build(); - } - - protected void addSectionDiagnosticResults() { - addSection(IpsSectionEnum.DIAGNOSTIC_RESULTS) - .withTitle("Diagnostic Results") - .withSectionCode("30954-2") - .withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative") - .withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults") - .build(); - } - - protected void addSectionVitalSigns() { - addSection(IpsSectionEnum.VITAL_SIGNS) - .withTitle("Vital Signs") - .withSectionCode("8716-3") - .withSectionDisplay("Vital signs") - .withResourceTypes(ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns") - .build(); - } - - protected void addSectionPregnancy() { - addSection(IpsSectionEnum.PREGNANCY) - .withTitle("Pregnancy Information") - .withSectionCode("10162-6") - .withSectionDisplay("History of pregnancies Narrative") - .withResourceTypes(ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx") - .build(); - } - - protected void addSectionSocialHistory() { - addSection(IpsSectionEnum.SOCIAL_HISTORY) - .withTitle("Social History") - .withSectionCode("29762-2") - .withSectionDisplay("Social history Narrative") - .withResourceTypes(ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory") - .build(); - } - - protected void addSectionIllnessHistory() { - addSection(IpsSectionEnum.ILLNESS_HISTORY) - .withTitle("History of Past Illness") - .withSectionCode("11348-0") - .withSectionDisplay("History of Past illness Narrative") - .withResourceTypes(ResourceType.Condition.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx") - .build(); - } - - protected void addSectionFunctionalStatus() { - addSection(IpsSectionEnum.FUNCTIONAL_STATUS) - .withTitle("Functional Status") - .withSectionCode("47420-5") - .withSectionDisplay("Functional status assessment note") - .withResourceTypes(ResourceType.ClinicalImpression.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus") - .build(); - } - - protected void addSectionPlanOfCare() { - addSection(IpsSectionEnum.PLAN_OF_CARE) - .withTitle("Plan of Care") - .withSectionCode("18776-5") - .withSectionDisplay("Plan of care note") - .withResourceTypes(ResourceType.CarePlan.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare") - .build(); - } - - protected void addSectionAdvanceDirectives() { - addSection(IpsSectionEnum.ADVANCE_DIRECTIVES) - .withTitle("Advance Directives") - .withSectionCode("42348-3") - .withSectionDisplay("Advance directives") - .withResourceTypes(ResourceType.Consent.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives") - .build(); - } - - private SectionBuilder addSection(IpsSectionEnum theSectionEnum) { - return new SectionBuilder(theSectionEnum); - } - - public SectionRegistry addGlobalCustomizer(Consumer theGlobalCustomizer) { - Validate.notNull(theGlobalCustomizer, "theGlobalCustomizer must not be null"); - myGlobalCustomizers.add(theGlobalCustomizer); - return this; - } - - public List
getSections() { - Validate.isTrue(isInitialized(), "Section registry has not been initialized"); - return Collections.unmodifiableList(mySections); - } - - public Section getSection(IpsSectionEnum theSectionEnum) { - return getSections().stream() - .filter(t -> t.getSectionEnum() == theSectionEnum) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No section for type: " + theSectionEnum)); - } - - public interface INoInfoGenerator { - - /** - * Generate an appropriate no-info resource. The resource does not need to have an ID populated, - * although it can if it is a resource found in the repository. - */ - IBaseResource generate(IIdType theSubjectId); - } - - public class SectionBuilder { - - private final IpsSectionEnum mySectionEnum; - private String myTitle; - private String mySectionCode; - private String mySectionDisplay; - private List myResourceTypes; - private String myProfile; - private INoInfoGenerator myNoInfoGenerator; - - public SectionBuilder(IpsSectionEnum theSectionEnum) { - mySectionEnum = theSectionEnum; - } - - public SectionBuilder withTitle(String theTitle) { - Validate.notBlank(theTitle); - myTitle = theTitle; - return this; - } - - public SectionBuilder withSectionCode(String theSectionCode) { - Validate.notBlank(theSectionCode); - mySectionCode = theSectionCode; - return this; - } - - public SectionBuilder withSectionDisplay(String theSectionDisplay) { - Validate.notBlank(theSectionDisplay); - mySectionDisplay = theSectionDisplay; - return this; - } - - public SectionBuilder withResourceTypes(String... theResourceTypes) { - Validate.isTrue(theResourceTypes.length > 0); - myResourceTypes = Arrays.asList(theResourceTypes); - return this; - } - - public SectionBuilder withProfile(String theProfile) { - Validate.notBlank(theProfile); - myProfile = theProfile; - return this; - } - - public SectionBuilder withNoInfoGenerator(INoInfoGenerator theNoInfoGenerator) { - myNoInfoGenerator = theNoInfoGenerator; - return this; - } - - public void build() { - myGlobalCustomizers.forEach(t -> t.accept(this)); - mySections.add(new Section( - mySectionEnum, - myTitle, - mySectionCode, - mySectionDisplay, - myResourceTypes, - myProfile, - myNoInfoGenerator)); - } - } - - private static class AllergyIntoleranceNoInfoR4Generator implements INoInfoGenerator { - @Override - public IBaseResource generate(IIdType theSubjectId) { - AllergyIntolerance allergy = new AllergyIntolerance(); - allergy.setCode(new CodeableConcept() - .addCoding(new Coding() - .setCode("no-allergy-info") - .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") - .setDisplay("No information about allergies"))) - .setPatient(new Reference(theSubjectId)) - .setClinicalStatus(new CodeableConcept() - .addCoding(new Coding() - .setCode("active") - .setSystem("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"))); - return allergy; - } - } - - private static class MedicationNoInfoR4Generator implements INoInfoGenerator { - @Override - public IBaseResource generate(IIdType theSubjectId) { - MedicationStatement medication = new MedicationStatement(); - // setMedicationCodeableConcept is not available - medication - .setMedication(new CodeableConcept() - .addCoding(new Coding() - .setCode("no-medication-info") - .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") - .setDisplay("No information about medications"))) - .setSubject(new Reference(theSubjectId)) - .setStatus(MedicationStatement.MedicationStatementStatus.UNKNOWN); - // .setEffective(new - // Period().addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/data-absent-reason").setValue((new Coding().setCode("not-applicable")))) - return medication; - } - } - - private static class ProblemNoInfoR4Generator implements INoInfoGenerator { - @Override - public IBaseResource generate(IIdType theSubjectId) { - Condition condition = new Condition(); - condition - .setCode(new CodeableConcept() - .addCoding(new Coding() - .setCode("no-problem-info") - .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") - .setDisplay("No information about problems"))) - .setSubject(new Reference(theSubjectId)) - .setClinicalStatus(new CodeableConcept() - .addCoding(new Coding() - .setCode("active") - .setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical"))); - return condition; - } - } - - public static class Section { - - private final IpsSectionEnum mySectionEnum; - private final String myTitle; - private final String mySectionCode; - private final String mySectionDisplay; - private final List myResourceTypes; - private final String myProfile; - private final INoInfoGenerator myNoInfoGenerator; - - public Section( - IpsSectionEnum theSectionEnum, - String theTitle, - String theSectionCode, - String theSectionDisplay, - List theResourceTypes, - String theProfile, - INoInfoGenerator theNoInfoGenerator) { - mySectionEnum = theSectionEnum; - myTitle = theTitle; - mySectionCode = theSectionCode; - mySectionDisplay = theSectionDisplay; - myResourceTypes = Collections.unmodifiableList(new ArrayList<>(theResourceTypes)); - myProfile = theProfile; - myNoInfoGenerator = theNoInfoGenerator; - } - - @Nullable - public INoInfoGenerator getNoInfoGenerator() { - return myNoInfoGenerator; - } - - public List getResourceTypes() { - return myResourceTypes; - } - - public String getProfile() { - return myProfile; - } - - public IpsSectionEnum getSectionEnum() { - return mySectionEnum; - } - - public String getTitle() { - return myTitle; - } - - public String getSectionCode() { - return mySectionCode; - } - - public String getSectionDisplay() { - return mySectionDisplay; - } - } -} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java index 1f53fcebd51..fcd0330319c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java @@ -30,11 +30,11 @@ public interface IIpsGeneratorSvc { * Generates an IPS document and returns the complete document bundle * for the given patient by ID */ - IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId); + IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile); /** * Generates an IPS document and returns the complete document bundle * for the given patient by identifier */ - IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier); + IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile); } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java index f3b837623c0..7562c5daf93 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java @@ -20,33 +20,23 @@ package ca.uhn.fhir.jpa.ips.generator; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; import ca.uhn.fhir.jpa.ips.api.IpsContext; -import ca.uhn.fhir.jpa.ips.api.IpsSectionEnum; -import ca.uhn.fhir.jpa.ips.api.SectionRegistry; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.dstu2.resource.Observation; -import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.api.Section; import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator; -import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.CompositionBuilder; import ca.uhn.fhir.util.ResourceReferenceInfo; -import ca.uhn.fhir.util.ValidateUtil; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseExtension; @@ -58,94 +48,100 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.InstantType; -import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; -import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { - public static final int CHUNK_SIZE = 10; - private static final Logger ourLog = LoggerFactory.getLogger(IpsGeneratorSvcImpl.class); - private final IIpsGenerationStrategy myGenerationStrategy; - private final DaoRegistry myDaoRegistry; + public static final String RESOURCE_ENTRY_INCLUSION_TYPE = "RESOURCE_ENTRY_INCLUSION_TYPE"; + public static final String URL_NARRATIVE_LINK = "http://hl7.org/fhir/StructureDefinition/narrativeLink"; + private final List myGenerationStrategies; private final FhirContext myFhirContext; /** * Constructor */ - public IpsGeneratorSvcImpl( - FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { - myGenerationStrategy = theGenerationStrategy; - myDaoRegistry = theDaoRegistry; + public IpsGeneratorSvcImpl(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) { + this(theFhirContext, List.of(theGenerationStrategy)); + } + + public IpsGeneratorSvcImpl(FhirContext theFhirContext, List theIpsGenerationStrategies) { + myGenerationStrategies = theIpsGenerationStrategies; myFhirContext = theFhirContext; + + myGenerationStrategies.forEach(IIpsGenerationStrategy::initialize); } + /** + * Generate an IPS using a patient ID + */ @Override - public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId) { - IBaseResource patient = myDaoRegistry.getResourceDao("Patient").read(thePatientId, theRequestDetails); - - return generateIpsForPatient(theRequestDetails, patient); + public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile) { + IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile); + IBaseResource patient = strategy.fetchPatient(thePatientId, theRequestDetails); + return generateIpsForPatient(strategy, theRequestDetails, patient); } + /** + * Generate an IPS using a patient identifier + */ @Override - public IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier) { - SearchParameterMap searchParameterMap = - new SearchParameterMap().setLoadSynchronousUpTo(2).add(Patient.SP_IDENTIFIER, thePatientIdentifier); - IBundleProvider searchResults = - myDaoRegistry.getResourceDao("Patient").search(searchParameterMap, theRequestDetails); - - ValidateUtil.isTrueOrThrowInvalidRequest( - searchResults.sizeOrThrowNpe() > 0, "No Patient could be found matching given identifier"); - ValidateUtil.isTrueOrThrowInvalidRequest( - searchResults.sizeOrThrowNpe() == 1, "Multiple Patient resources were found matching given identifier"); - - IBaseResource patient = searchResults.getResources(0, 1).get(0); - - return generateIpsForPatient(theRequestDetails, patient); + public IBaseBundle generateIps( + RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile) { + IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile); + IBaseResource patient = strategy.fetchPatient(thePatientIdentifier, theRequestDetails); + return generateIpsForPatient(strategy, theRequestDetails, patient); } - private IBaseBundle generateIpsForPatient(RequestDetails theRequestDetails, IBaseResource thePatient) { + IIpsGenerationStrategy selectGenerationStrategy(@Nullable String theRequestedProfile) { + return myGenerationStrategies.stream() + .filter(t -> isBlank(theRequestedProfile) || theRequestedProfile.equals(t.getBundleProfile())) + .findFirst() + .orElse(myGenerationStrategies.get(0)); + } + + private IBaseBundle generateIpsForPatient( + IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, IBaseResource thePatient) { IIdType originalSubjectId = myFhirContext .getVersion() .newIdType() .setValue(thePatient.getIdElement().getValue()) .toUnqualifiedVersionless(); - massageResourceId(null, thePatient); + massageResourceId(theStrategy, theRequestDetails, null, thePatient); IpsContext context = new IpsContext(thePatient, originalSubjectId); ResourceInclusionCollection globalResourcesToInclude = new ResourceInclusionCollection(); globalResourcesToInclude.addResourceIfNotAlreadyPresent(thePatient, originalSubjectId.getValue()); - IBaseResource author = myGenerationStrategy.createAuthor(); - massageResourceId(context, author); + IBaseResource author = theStrategy.createAuthor(); + massageResourceId(theStrategy, theRequestDetails, context, author); - CompositionBuilder compositionBuilder = createComposition(thePatient, context, author); - determineInclusions( - theRequestDetails, originalSubjectId, context, compositionBuilder, globalResourcesToInclude); + CompositionBuilder compositionBuilder = createComposition(theStrategy, thePatient, context, author); + determineInclusions(theStrategy, theRequestDetails, context, compositionBuilder, globalResourcesToInclude); IBaseResource composition = compositionBuilder.getComposition(); // Create the narrative for the Composition itself - CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(globalResourcesToInclude); + CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, globalResourcesToInclude); generator.populateResourceNarrative(myFhirContext, composition); - return createCompositionDocument(author, composition, globalResourcesToInclude); + return createDocumentBundleForComposition(theStrategy, author, composition, globalResourcesToInclude); } - private IBaseBundle createCompositionDocument( - IBaseResource author, IBaseResource composition, ResourceInclusionCollection theResourcesToInclude) { + private IBaseBundle createDocumentBundleForComposition( + IIpsGenerationStrategy theStrategy, + IBaseResource author, + IBaseResource composition, + ResourceInclusionCollection theResourcesToInclude) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); bundleBuilder.setType(Bundle.BundleType.DOCUMENT.toCode()); bundleBuilder.setIdentifier("urn:ietf:rfc:4122", UUID.randomUUID().toString()); @@ -162,124 +158,51 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { // Add author to document bundleBuilder.addDocumentEntry(author); - return bundleBuilder.getBundle(); + IBaseBundle retVal = bundleBuilder.getBundle(); + + theStrategy.postManipulateIpsBundle(retVal); + + return retVal; } - @Nonnull - private ResourceInclusionCollection determineInclusions( + private void determineInclusions( + IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, - IIdType originalSubjectId, - IpsContext context, + IpsContext theIpsContext, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theGlobalResourcesToInclude) { - SectionRegistry sectionRegistry = myGenerationStrategy.getSectionRegistry(); - for (SectionRegistry.Section nextSection : sectionRegistry.getSections()) { + for (Section nextSection : theStrategy.getSections()) { determineInclusionsForSection( + theStrategy, theRequestDetails, - originalSubjectId, - context, + theIpsContext, theCompositionBuilder, theGlobalResourcesToInclude, nextSection); } - return theGlobalResourcesToInclude; } private void determineInclusionsForSection( + IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, - IIdType theOriginalSubjectId, IpsContext theIpsContext, CompositionBuilder theCompositionBuilder, - ResourceInclusionCollection theGlobalResourcesToInclude, - SectionRegistry.Section theSection) { - ResourceInclusionCollection sectionResourcesToInclude = new ResourceInclusionCollection(); - for (String nextResourceType : theSection.getResourceTypes()) { + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection) { + ResourceInclusionCollection sectionResourceCollectionToPopulate = new ResourceInclusionCollection(); + ISectionResourceSupplier resourceSupplier = theStrategy.getSectionResourceSupplier(theSection); - SearchParameterMap searchParameterMap = new SearchParameterMap(); - String subjectSp = determinePatientCompartmentSearchParameterName(nextResourceType); - searchParameterMap.add(subjectSp, new ReferenceParam(theOriginalSubjectId)); + determineInclusionsForSectionResourceTypes( + theStrategy, + theRequestDetails, + theIpsContext, + theGlobalResourceCollectionToPopulate, + theSection, + resourceSupplier, + sectionResourceCollectionToPopulate); - IpsSectionEnum sectionEnum = theSection.getSectionEnum(); - IpsContext.IpsSectionContext ipsSectionContext = - theIpsContext.newSectionContext(sectionEnum, nextResourceType); - myGenerationStrategy.massageResourceSearch(ipsSectionContext, searchParameterMap); - - Set includes = myGenerationStrategy.provideResourceSearchIncludes(ipsSectionContext); - includes.forEach(searchParameterMap::addInclude); - - IFhirResourceDao dao = myDaoRegistry.getResourceDao(nextResourceType); - IBundleProvider searchResult = dao.search(searchParameterMap, theRequestDetails); - for (int startIndex = 0; ; startIndex += CHUNK_SIZE) { - int endIndex = startIndex + CHUNK_SIZE; - List resources = searchResult.getResources(startIndex, endIndex); - if (resources.isEmpty()) { - break; - } - - for (IBaseResource nextCandidate : resources) { - - boolean candidateIsSearchInclude = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate) - == BundleEntrySearchModeEnum.INCLUDE; - boolean addResourceToBundle; - if (candidateIsSearchInclude) { - addResourceToBundle = true; - } else { - addResourceToBundle = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate); - } - - if (addResourceToBundle) { - - String originalResourceId = nextCandidate - .getIdElement() - .toUnqualifiedVersionless() - .getValue(); - - // Check if we already have this resource included so that we don't - // include it twice - IBaseResource previouslyExistingResource = - theGlobalResourcesToInclude.getResourceByOriginalId(originalResourceId); - if (previouslyExistingResource != null) { - BundleEntrySearchModeEnum candidateSearchEntryMode = - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate); - if (candidateSearchEntryMode == BundleEntrySearchModeEnum.MATCH) { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put( - previouslyExistingResource, BundleEntrySearchModeEnum.MATCH); - } - - nextCandidate = previouslyExistingResource; - sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); - } else if (theGlobalResourcesToInclude.hasResourceWithReplacementId(originalResourceId)) { - if (!candidateIsSearchInclude) { - sectionResourcesToInclude.addResourceIfNotAlreadyPresent( - nextCandidate, originalResourceId); - } - } else { - IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, nextCandidate); - nextCandidate.setId(id); - theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent( - nextCandidate, originalResourceId); - if (!candidateIsSearchInclude) { - sectionResourcesToInclude.addResourceIfNotAlreadyPresent( - nextCandidate, originalResourceId); - } - } - } - } - } - } - - if (sectionResourcesToInclude.isEmpty() && theSection.getNoInfoGenerator() != null) { - IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId()); - String id = IdType.newRandomUuid().getValue(); - if (noInfoResource.getIdElement().isEmpty()) { - noInfoResource.setId(id); - } - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(noInfoResource, BundleEntrySearchModeEnum.MATCH); - theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent( - noInfoResource, - noInfoResource.getIdElement().toUnqualifiedVersionless().getValue()); - sectionResourcesToInclude.addResourceIfNotAlreadyPresent(noInfoResource, id); - } + generateSectionNoInfoResourceIfNoInclusionsFound( + theIpsContext, theGlobalResourceCollectionToPopulate, theSection, sectionResourceCollectionToPopulate); /* * Update any references within the added candidates - This is important @@ -287,7 +210,23 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { * the summary, so we need to also update the references to those * resources. */ - for (IBaseResource nextResource : theGlobalResourcesToInclude.getResources()) { + updateReferencesInInclusionsForSection(theGlobalResourceCollectionToPopulate); + + if (sectionResourceCollectionToPopulate.isEmpty()) { + return; + } + + addSection( + theStrategy, + theSection, + theCompositionBuilder, + sectionResourceCollectionToPopulate, + theGlobalResourceCollectionToPopulate); + } + + private void updateReferencesInInclusionsForSection( + ResourceInclusionCollection theGlobalResourceCollectionToPopulate) { + for (IBaseResource nextResource : theGlobalResourceCollectionToPopulate.getResources()) { List references = myFhirContext.newTerser().getAllResourceReferences(nextResource); for (ResourceReferenceInfo nextReference : references) { String existingReference = nextReference @@ -298,12 +237,12 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { existingReference = new IdType(existingReference) .toUnqualifiedVersionless() .getValue(); - String replacement = theGlobalResourcesToInclude.getIdSubstitution(existingReference); + String replacement = theGlobalResourceCollectionToPopulate.getIdSubstitution(existingReference); if (isNotBlank(replacement)) { if (!replacement.equals(existingReference)) { nextReference.getResourceReference().setReference(replacement); } - } else if (theGlobalResourcesToInclude.getResourceById(existingReference) == null) { + } else if (theGlobalResourceCollectionToPopulate.getResourceById(existingReference) == null) { // If this reference doesn't point to something we have actually // included in the bundle, clear the reference. nextReference.getResourceReference().setReference(null); @@ -312,17 +251,184 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { } } } + } - if (sectionResourcesToInclude.isEmpty()) { - return; + private static void generateSectionNoInfoResourceIfNoInclusionsFound( + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection, + ResourceInclusionCollection sectionResourceCollectionToPopulate) { + if (sectionResourceCollectionToPopulate.isEmpty() && theSection.getNoInfoGenerator() != null) { + IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId()); + String id = IdType.newRandomUuid().getValue(); + if (noInfoResource.getIdElement().isEmpty()) { + noInfoResource.setId(id); + } + noInfoResource.setUserData( + RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); + theGlobalResourceCollectionToPopulate.addResourceIfNotAlreadyPresent( + noInfoResource, + noInfoResource.getIdElement().toUnqualifiedVersionless().getValue()); + sectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(noInfoResource, id); + } + } + + private void determineInclusionsForSectionResourceTypes( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection, + ISectionResourceSupplier resourceSupplier, + ResourceInclusionCollection sectionResourceCollectionToPopulate) { + for (Class nextResourceType : theSection.getResourceTypes()) { + determineInclusionsForSectionResourceType( + theStrategy, + theRequestDetails, + theIpsContext, + theGlobalResourceCollectionToPopulate, + theSection, + nextResourceType, + resourceSupplier, + sectionResourceCollectionToPopulate); + } + } + + private void determineInclusionsForSectionResourceType( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection, + Class nextResourceType, + ISectionResourceSupplier resourceSupplier, + ResourceInclusionCollection sectionResourceCollectionToPopulate) { + IpsSectionContext ipsSectionContext = theIpsContext.newSectionContext(theSection, nextResourceType); + + List resources = + resourceSupplier.fetchResourcesForSection(theIpsContext, ipsSectionContext, theRequestDetails); + if (resources != null) { + for (ISectionResourceSupplier.ResourceEntry nextEntry : resources) { + IBaseResource resource = nextEntry.getResource(); + Validate.isTrue( + resource.getIdElement().hasIdPart(), + "fetchResourcesForSection(..) returned resource(s) with no ID populated"); + resource.setUserData(RESOURCE_ENTRY_INCLUSION_TYPE, nextEntry.getInclusionType()); + } + addResourcesToIpsContents( + theStrategy, + theRequestDetails, + theIpsContext, + resources, + theGlobalResourceCollectionToPopulate, + sectionResourceCollectionToPopulate); + } + } + + /** + * Given a collection of resources that have been fetched, analyze them and add them as appropriate + * to the collection that will be included in a given IPS section context. + * + * @param theStrategy The generation strategy + * @param theIpsContext The overall IPS generation context for this IPS. + * @param theCandidateResources The resources that have been fetched for inclusion in the IPS bundle + */ + private void addResourcesToIpsContents( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + List theCandidateResources, + ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, + ResourceInclusionCollection theSectionResourceCollectionToPopulate) { + for (ISectionResourceSupplier.ResourceEntry nextCandidateEntry : theCandidateResources) { + if (nextCandidateEntry.getInclusionType() == ISectionResourceSupplier.InclusionTypeEnum.EXCLUDE) { + continue; + } + + IBaseResource nextCandidate = nextCandidateEntry.getResource(); + boolean primaryResource = nextCandidateEntry.getInclusionType() + == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE; + + String originalResourceId = + nextCandidate.getIdElement().toUnqualifiedVersionless().getValue(); + + // Check if we already have this resource included so that we don't + // include it twice + IBaseResource previouslyExistingResource = + theGlobalResourcesCollectionToPopulate.getResourceByOriginalId(originalResourceId); + + if (previouslyExistingResource != null) { + reuseAlreadyIncludedGlobalResourceInSectionCollection( + theSectionResourceCollectionToPopulate, + previouslyExistingResource, + primaryResource, + originalResourceId); + } else if (theGlobalResourcesCollectionToPopulate.hasResourceWithReplacementId(originalResourceId)) { + addResourceToSectionCollectionOnlyIfPrimary( + theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); + } else { + addResourceToGlobalCollectionAndSectionCollection( + theStrategy, + theRequestDetails, + theIpsContext, + theGlobalResourcesCollectionToPopulate, + theSectionResourceCollectionToPopulate, + nextCandidate, + originalResourceId, + primaryResource); + } + } + } + + private static void addResourceToSectionCollectionOnlyIfPrimary( + ResourceInclusionCollection theSectionResourceCollectionToPopulate, + boolean primaryResource, + IBaseResource nextCandidate, + String originalResourceId) { + if (primaryResource) { + theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); + } + } + + private void addResourceToGlobalCollectionAndSectionCollection( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, + ResourceInclusionCollection theSectionResourceCollectionToPopulate, + IBaseResource nextCandidate, + String originalResourceId, + boolean primaryResource) { + massageResourceId(theStrategy, theRequestDetails, theIpsContext, nextCandidate); + theGlobalResourcesCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); + addResourceToSectionCollectionOnlyIfPrimary( + theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); + } + + private static void reuseAlreadyIncludedGlobalResourceInSectionCollection( + ResourceInclusionCollection theSectionResourceCollectionToPopulate, + IBaseResource previouslyExistingResource, + boolean primaryResource, + String originalResourceId) { + IBaseResource nextCandidate; + ISectionResourceSupplier.InclusionTypeEnum previouslyIncludedResourceInclusionType = + (ISectionResourceSupplier.InclusionTypeEnum) + previouslyExistingResource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); + if (previouslyIncludedResourceInclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { + if (primaryResource) { + previouslyExistingResource.setUserData( + RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); + } } - addSection(theSection, theCompositionBuilder, sectionResourcesToInclude, theGlobalResourcesToInclude); + nextCandidate = previouslyExistingResource; + theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); } @SuppressWarnings("unchecked") private void addSection( - SectionRegistry.Section theSection, + IIpsGenerationStrategy theStrategy, + Section theSection, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theResourcesToInclude, ResourceInclusionCollection theGlobalResourcesToInclude) { @@ -330,34 +436,44 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection(); sectionBuilder.setTitle(theSection.getTitle()); - sectionBuilder.addCodeCoding(LOINC_URI, theSection.getSectionCode(), theSection.getSectionDisplay()); + sectionBuilder.addCodeCoding( + theSection.getSectionSystem(), theSection.getSectionCode(), theSection.getSectionDisplay()); for (IBaseResource next : theResourcesToInclude.getResources()) { - if (ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next) == BundleEntrySearchModeEnum.INCLUDE) { + ISectionResourceSupplier.InclusionTypeEnum inclusionType = + (ISectionResourceSupplier.InclusionTypeEnum) next.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); + if (inclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { continue; } - IBaseExtension narrativeLink = ((IBaseHasExtensions) next).addExtension(); - narrativeLink.setUrl("http://hl7.org/fhir/StructureDefinition/narrativeLink"); - String narrativeLinkValue = - theCompositionBuilder.getComposition().getIdElement().getValue() - + "#" - + myFhirContext.getResourceType(next) - + "-" - + next.getIdElement().getValue(); - IPrimitiveType narrativeLinkUri = (IPrimitiveType) - myFhirContext.getElementDefinition("url").newInstance(); - narrativeLinkUri.setValueAsString(narrativeLinkValue); - narrativeLink.setValue(narrativeLinkUri); + IBaseHasExtensions extensionHolder = (IBaseHasExtensions) next; + if (extensionHolder.getExtension().stream() + .noneMatch(t -> t.getUrl().equals(URL_NARRATIVE_LINK))) { + IBaseExtension narrativeLink = extensionHolder.addExtension(); + narrativeLink.setUrl(URL_NARRATIVE_LINK); + String narrativeLinkValue = + theCompositionBuilder.getComposition().getIdElement().getValue() + + "#" + + myFhirContext.getResourceType(next) + + "-" + + next.getIdElement().getValue(); + IPrimitiveType narrativeLinkUri = + (IPrimitiveType) requireNonNull(myFhirContext.getElementDefinition("url")) + .newInstance(); + narrativeLinkUri.setValueAsString(narrativeLinkValue); + narrativeLink.setValue(narrativeLinkUri); + } sectionBuilder.addEntry(next.getIdElement()); } - String narrative = createSectionNarrative(theSection, theResourcesToInclude, theGlobalResourcesToInclude); + String narrative = + createSectionNarrative(theStrategy, theSection, theResourcesToInclude, theGlobalResourcesToInclude); sectionBuilder.setText("generated", narrative); } - private CompositionBuilder createComposition(IBaseResource thePatient, IpsContext context, IBaseResource author) { + private CompositionBuilder createComposition( + IIpsGenerationStrategy theStrategy, IBaseResource thePatient, IpsContext context, IBaseResource author) { CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext); compositionBuilder.setId(IdType.newRandomUuid()); @@ -365,43 +481,44 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { compositionBuilder.setSubject(thePatient.getIdElement().toUnqualifiedVersionless()); compositionBuilder.addTypeCoding("http://loinc.org", "60591-5", "Patient Summary Document"); compositionBuilder.setDate(InstantType.now()); - compositionBuilder.setTitle(myGenerationStrategy.createTitle(context)); - compositionBuilder.setConfidentiality(myGenerationStrategy.createConfidentiality(context)); + compositionBuilder.setTitle(theStrategy.createTitle(context)); + compositionBuilder.setConfidentiality(theStrategy.createConfidentiality(context)); compositionBuilder.addAuthor(author.getIdElement()); return compositionBuilder; } - private String determinePatientCompartmentSearchParameterName(String theResourceType) { - RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType); - Set searchParams = resourceDef.getSearchParamsForCompartmentName("Patient").stream() - .map(RuntimeSearchParam::getName) - .collect(Collectors.toSet()); - // Prefer "patient", then "subject" then anything else - if (searchParams.contains(Observation.SP_PATIENT)) { - return Observation.SP_PATIENT; - } - if (searchParams.contains(Observation.SP_SUBJECT)) { - return Observation.SP_SUBJECT; - } - return searchParams.iterator().next(); - } + private void massageResourceId( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + IBaseResource theResource) { + String base = theRequestDetails.getFhirServerBase(); - private void massageResourceId(IpsContext theIpsContext, IBaseResource theResource) { - IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, theResource); - theResource.setId(id); + IIdType id = theResource.getIdElement(); + if (!id.hasBaseUrl() && id.hasResourceType() && id.hasIdPart()) { + id = id.withServerBase(base, id.getResourceType()); + theResource.setId(id); + } + + id = theStrategy.massageResourceId(theIpsContext, theResource); + if (id != null) { + theResource.setId(id); + } } private String createSectionNarrative( - SectionRegistry.Section theSection, + IIpsGenerationStrategy theStrategy, + Section theSection, ResourceInclusionCollection theResources, ResourceInclusionCollection theGlobalResourceCollection) { - CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theGlobalResourceCollection); + CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, theGlobalResourceCollection); Bundle bundle = new Bundle(); for (IBaseResource resource : theResources.getResources()) { - BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(resource); - if (searchMode == BundleEntrySearchModeEnum.MATCH) { + ISectionResourceSupplier.InclusionTypeEnum inclusionType = + (ISectionResourceSupplier.InclusionTypeEnum) resource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); + if (inclusionType == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { bundle.addEntry().setResource((Resource) resource); } } @@ -414,14 +531,13 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { @Nonnull private CustomThymeleafNarrativeGenerator newNarrativeGenerator( - ResourceInclusionCollection theGlobalResourceCollection) { - List narrativePropertyFiles = myGenerationStrategy.getNarrativePropertyFiles(); + IIpsGenerationStrategy theStrategy, ResourceInclusionCollection theGlobalResourceCollection) { + List narrativePropertyFiles = theStrategy.getNarrativePropertyFiles(); CustomThymeleafNarrativeGenerator generator = new CustomThymeleafNarrativeGenerator(narrativePropertyFiles); generator.setFhirPathEvaluationContext(new IFhirPathEvaluationContext() { @Override public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { - IBaseResource resource = theGlobalResourceCollection.getResourceById(theReference); - return resource; + return theGlobalResourceCollection.getResourceById(theReference); } }); return generator; diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java new file mode 100644 index 00000000000..6a9368e052e --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java @@ -0,0 +1,477 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.ips.api.Section; +import ca.uhn.fhir.jpa.ips.jpa.section.AdvanceDirectivesJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.AllergyIntoleranceJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport; +import ca.uhn.fhir.jpa.ips.jpa.section.DiagnosticResultsJpaSectionSearchStrategyObservation; +import ca.uhn.fhir.jpa.ips.jpa.section.FunctionalStatusJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.IllnessHistoryJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.ImmunizationsJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicalDevicesJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationAdministration; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationDispense; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationRequest; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationStatement; +import ca.uhn.fhir.jpa.ips.jpa.section.PlanOfCareJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.PregnancyJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.ProblemListJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.ProceduresJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.SocialHistoryJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.VitalSignsJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.strategy.AllergyIntoleranceNoInfoR4Generator; +import ca.uhn.fhir.jpa.ips.strategy.BaseIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.strategy.MedicationNoInfoR4Generator; +import ca.uhn.fhir.jpa.ips.strategy.ProblemNoInfoR4Generator; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.ValidateUtil; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.ClinicalImpression; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.DeviceUseStatement; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Immunization; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.MedicationDispense; +import org.hl7.fhir.r4.model.MedicationRequest; +import org.hl7.fhir.r4.model.MedicationStatement; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Procedure; +import org.springframework.beans.factory.annotation.Autowired; +import org.thymeleaf.util.Validate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.function.Function; + +/** + * This {@link ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy generation strategy} contains default rules for fetching + * IPS section contents for each of the base (universal realm) IPS definition sections. It fetches contents for each + * section from the JPA server repository. + *

+ * This class can be used directly, but it can also be subclassed and extended if you want to + * create an IPS strategy that is based on the defaults but add or change the inclusion rules or + * sections. If you are subclassing this class, the typical approach is to override the + * {@link #addSections()} method and replace it with your own implementation. You can include + * any of the same sections that are defined in the parent class, but you can also omit any + * you don't want to include, and add your own as well. + *

+ */ +public class DefaultJpaIpsGenerationStrategy extends BaseIpsGenerationStrategy { + + public static final String SECTION_CODE_ALLERGY_INTOLERANCE = "48765-2"; + public static final String SECTION_CODE_MEDICATION_SUMMARY = "10160-0"; + public static final String SECTION_CODE_PROBLEM_LIST = "11450-4"; + public static final String SECTION_CODE_IMMUNIZATIONS = "11369-6"; + public static final String SECTION_CODE_PROCEDURES = "47519-4"; + public static final String SECTION_CODE_MEDICAL_DEVICES = "46264-8"; + public static final String SECTION_CODE_DIAGNOSTIC_RESULTS = "30954-2"; + public static final String SECTION_CODE_VITAL_SIGNS = "8716-3"; + public static final String SECTION_CODE_PREGNANCY = "10162-6"; + public static final String SECTION_CODE_SOCIAL_HISTORY = "29762-2"; + public static final String SECTION_CODE_ILLNESS_HISTORY = "11348-0"; + public static final String SECTION_CODE_FUNCTIONAL_STATUS = "47420-5"; + public static final String SECTION_CODE_PLAN_OF_CARE = "18776-5"; + public static final String SECTION_CODE_ADVANCE_DIRECTIVES = "42348-3"; + public static final String SECTION_SYSTEM_LOINC = ITermLoaderSvc.LOINC_URI; + private final List> myGlobalSectionCustomizers = new ArrayList<>(); + + @Autowired + private DaoRegistry myDaoRegistry; + + @Autowired + private FhirContext myFhirContext; + + private boolean myInitialized; + + public void setDaoRegistry(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; + } + + public void setFhirContext(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + /** + * Subclasses may call this method to add customers that will customize every section + * added to the strategy. + */ + public void addGlobalSectionCustomizer(@Nonnull Function theCustomizer) { + Validate.isTrue(!myInitialized, "This method must not be called after the strategy is initialized"); + Validate.notNull(theCustomizer, "theCustomizer must not be null"); + myGlobalSectionCustomizers.add(theCustomizer); + } + + @Override + public final void initialize() { + Validate.isTrue(!myInitialized, "Strategy must not be initialized twice"); + Validate.isTrue(myDaoRegistry != null, "No DaoRegistry has been supplied"); + Validate.isTrue(myFhirContext != null, "No FhirContext has been supplied"); + addSections(); + myInitialized = true; + } + + @Nonnull + @Override + public IBaseResource fetchPatient(IIdType thePatientId, RequestDetails theRequestDetails) { + return myDaoRegistry.getResourceDao("Patient").read(thePatientId, theRequestDetails); + } + + @Nonnull + @Override + public IBaseResource fetchPatient(TokenParam thePatientIdentifier, RequestDetails theRequestDetails) { + SearchParameterMap searchParameterMap = + new SearchParameterMap().setLoadSynchronousUpTo(2).add(Patient.SP_IDENTIFIER, thePatientIdentifier); + IBundleProvider searchResults = + myDaoRegistry.getResourceDao("Patient").search(searchParameterMap, theRequestDetails); + + ValidateUtil.isTrueOrThrowResourceNotFound( + searchResults.sizeOrThrowNpe() > 0, "No Patient could be found matching given identifier"); + ValidateUtil.isTrueOrThrowInvalidRequest( + searchResults.sizeOrThrowNpe() == 1, "Multiple Patient resources were found matching given identifier"); + + return searchResults.getResources(0, 1).get(0); + } + + /** + * Add the various sections to the registry in order. This method can be overridden for + * customization. + */ + protected void addSections() { + addJpaSectionAllergyIntolerance(); + addJpaSectionMedicationSummary(); + addJpaSectionProblemList(); + addJpaSectionImmunizations(); + addJpaSectionProcedures(); + addJpaSectionMedicalDevices(); + addJpaSectionDiagnosticResults(); + addJpaSectionVitalSigns(); + addJpaSectionPregnancy(); + addJpaSectionSocialHistory(); + addJpaSectionIllnessHistory(); + addJpaSectionFunctionalStatus(); + addJpaSectionPlanOfCare(); + addJpaSectionAdvanceDirectives(); + } + + protected void addJpaSectionAllergyIntolerance() { + Section section = Section.newBuilder() + .withTitle("Allergies and Intolerances") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_ALLERGY_INTOLERANCE) + .withSectionDisplay("Allergies and adverse reactions Document") + .withResourceType(AllergyIntolerance.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies") + .withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator()) + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(AllergyIntolerance.class, new AllergyIntoleranceJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionMedicationSummary() { + Section section = Section.newBuilder() + .withTitle("Medication List") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_MEDICATION_SUMMARY) + .withSectionDisplay("History of Medication use Narrative") + .withResourceType(MedicationStatement.class) + .withResourceType(MedicationRequest.class) + .withResourceType(MedicationAdministration.class) + .withResourceType(MedicationDispense.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications") + .withNoInfoGenerator(new MedicationNoInfoR4Generator()) + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy( + MedicationAdministration.class, + new MedicationSummaryJpaSectionSearchStrategyMedicationAdministration()) + .addStrategy( + MedicationDispense.class, new MedicationSummaryJpaSectionSearchStrategyMedicationDispense()) + .addStrategy(MedicationRequest.class, new MedicationSummaryJpaSectionSearchStrategyMedicationRequest()) + .addStrategy( + MedicationStatement.class, new MedicationSummaryJpaSectionSearchStrategyMedicationStatement()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionProblemList() { + Section section = Section.newBuilder() + .withTitle("Problem List") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PROBLEM_LIST) + .withSectionDisplay("Problem list - Reported") + .withResourceType(Condition.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems") + .withNoInfoGenerator(new ProblemNoInfoR4Generator()) + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Condition.class, new ProblemListJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionImmunizations() { + Section section = Section.newBuilder() + .withTitle("History of Immunizations") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_IMMUNIZATIONS) + .withSectionDisplay("History of Immunization Narrative") + .withResourceType(Immunization.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Immunization.class, new ImmunizationsJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionProcedures() { + Section section = Section.newBuilder() + .withTitle("History of Procedures") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PROCEDURES) + .withSectionDisplay("History of Procedures Document") + .withResourceType(Procedure.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Procedure.class, new ProceduresJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionMedicalDevices() { + Section section = Section.newBuilder() + .withTitle("Medical Devices") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_MEDICAL_DEVICES) + .withSectionDisplay("History of medical device use") + .withResourceType(DeviceUseStatement.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(DeviceUseStatement.class, new MedicalDevicesJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionDiagnosticResults() { + Section section = Section.newBuilder() + .withTitle("Diagnostic Results") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_DIAGNOSTIC_RESULTS) + .withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative") + .withResourceType(DiagnosticReport.class) + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(DiagnosticReport.class, new DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport()) + .addStrategy(Observation.class, new DiagnosticResultsJpaSectionSearchStrategyObservation()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionVitalSigns() { + Section section = Section.newBuilder() + .withTitle("Vital Signs") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_VITAL_SIGNS) + .withSectionDisplay("Vital signs") + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Observation.class, new VitalSignsJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionPregnancy() { + Section section = Section.newBuilder() + .withTitle("Pregnancy Information") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PREGNANCY) + .withSectionDisplay("History of pregnancies Narrative") + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Observation.class, new PregnancyJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionSocialHistory() { + Section section = Section.newBuilder() + .withTitle("Social History") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_SOCIAL_HISTORY) + .withSectionDisplay("Social history Narrative") + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Observation.class, new SocialHistoryJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionIllnessHistory() { + Section section = Section.newBuilder() + .withTitle("History of Past Illness") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_ILLNESS_HISTORY) + .withSectionDisplay("History of Past illness Narrative") + .withResourceType(Condition.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Condition.class, new IllnessHistoryJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionFunctionalStatus() { + Section section = Section.newBuilder() + .withTitle("Functional Status") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_FUNCTIONAL_STATUS) + .withSectionDisplay("Functional status assessment note") + .withResourceType(ClinicalImpression.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(ClinicalImpression.class, new FunctionalStatusJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionPlanOfCare() { + Section section = Section.newBuilder() + .withTitle("Plan of Care") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PLAN_OF_CARE) + .withSectionDisplay("Plan of care note") + .withResourceType(CarePlan.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(CarePlan.class, new PlanOfCareJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionAdvanceDirectives() { + Section section = Section.newBuilder() + .withTitle("Advance Directives") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_ADVANCE_DIRECTIVES) + .withSectionDisplay("Advance directives") + .withResourceType(Consent.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Consent.class, new AdvanceDirectivesJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSection( + Section theSection, JpaSectionSearchStrategyCollection theSectionSearchStrategyCollection) { + Section section = theSection; + for (var next : myGlobalSectionCustomizers) { + section = next.apply(section); + } + + Validate.isTrue( + theSection.getResourceTypes().size() + == theSectionSearchStrategyCollection.getResourceTypes().size(), + "Search strategy types does not match section types"); + Validate.isTrue( + new HashSet<>(theSection.getResourceTypes()) + .containsAll(theSectionSearchStrategyCollection.getResourceTypes()), + "Search strategy types does not match section types"); + + addSection( + section, + new JpaSectionResourceSupplier(theSectionSearchStrategyCollection, myDaoRegistry, myFhirContext)); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..b6294bc499e --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java @@ -0,0 +1,68 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; + +/** + * Implementations of this interface are used to fetch resources to include + * for a given IPS section by performing a search in a local JPA repository. + * + * @since 7.2.0 + */ +public interface IJpaSectionSearchStrategy { + + /** + * This method can manipulate the {@link SearchParameterMap} that will + * be used to find candidate resources for the given IPS section. The map will already have + * a subject/patient parameter added to it. The map provided in {@literal theSearchParameterMap} + * will contain a subject/patient reference (e.g. ?patient=Patient/123), but no + * other parameters. This method can add other parameters. The default implementation of this + * interface performs no action. + *

+ * For example, for a Vital Signs section, the implementation might add a parameter indicating + * the parameter category=vital-signs. + *

+ * + * @param theIpsSectionContext The context, which indicates the IPS section and the resource type + * being searched for. + * @param theSearchParameterMap The map to manipulate. + */ + default void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull SearchParameterMap theSearchParameterMap) { + // no action taken by default + } + + /** + * This method will be called for each found resource candidate for inclusion in the + * IPS document. The strategy can decide whether to include it or not. Note that the + * default implementation will always return {@literal true}. + *

+ * This method is called once for every resource that is being considered for inclusion + * in an IPS section. + *

+ */ + default boolean shouldInclude(@Nonnull IpsSectionContext theIpsSectionContext, @Nonnull T theCandidate) { + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java new file mode 100644 index 00000000000..2f365aae674 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java @@ -0,0 +1,25 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IpsGenerationCtxConfig {} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java new file mode 100644 index 00000000000..e50bec7c1ac --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java @@ -0,0 +1,128 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; +import ca.uhn.fhir.jpa.ips.api.IpsContext; +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.ReferenceParam; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Coverage; +import org.thymeleaf.util.Validate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class JpaSectionResourceSupplier implements ISectionResourceSupplier { + public static final int CHUNK_SIZE = 10; + + private final JpaSectionSearchStrategyCollection mySectionSearchStrategyCollection; + private final DaoRegistry myDaoRegistry; + private final FhirContext myFhirContext; + + public JpaSectionResourceSupplier( + @Nonnull JpaSectionSearchStrategyCollection theSectionSearchStrategyCollection, + @Nonnull DaoRegistry theDaoRegistry, + @Nonnull FhirContext theFhirContext) { + Validate.notNull(theSectionSearchStrategyCollection, "theSectionSearchStrategyCollection must not be null"); + Validate.notNull(theDaoRegistry, "theDaoRegistry must not be null"); + Validate.notNull(theFhirContext, "theFhirContext must not be null"); + mySectionSearchStrategyCollection = theSectionSearchStrategyCollection; + myDaoRegistry = theDaoRegistry; + myFhirContext = theFhirContext; + } + + @Nullable + @Override + public List fetchResourcesForSection( + IpsContext theIpsContext, IpsSectionContext theIpsSectionContext, RequestDetails theRequestDetails) { + + IJpaSectionSearchStrategy searchStrategy = + mySectionSearchStrategyCollection.getSearchStrategy(theIpsSectionContext.getResourceType()); + + SearchParameterMap searchParameterMap = new SearchParameterMap(); + + String subjectSp = determinePatientCompartmentSearchParameterName(theIpsSectionContext.getResourceType()); + searchParameterMap.add(subjectSp, new ReferenceParam(theIpsContext.getSubjectId())); + + searchStrategy.massageResourceSearch(theIpsSectionContext, searchParameterMap); + + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theIpsSectionContext.getResourceType()); + IBundleProvider searchResult = dao.search(searchParameterMap, theRequestDetails); + + List retVal = null; + for (int startIndex = 0; ; startIndex += CHUNK_SIZE) { + int endIndex = startIndex + CHUNK_SIZE; + List resources = searchResult.getResources(startIndex, endIndex); + if (resources.isEmpty()) { + break; + } + + for (IBaseResource next : resources) { + if (!next.getClass().isAssignableFrom(theIpsSectionContext.getResourceType()) + || searchStrategy.shouldInclude(theIpsSectionContext, (T) next)) { + if (retVal == null) { + retVal = new ArrayList<>(); + } + InclusionTypeEnum inclusionType = + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next) == BundleEntrySearchModeEnum.INCLUDE + ? InclusionTypeEnum.SECONDARY_RESOURCE + : InclusionTypeEnum.PRIMARY_RESOURCE; + retVal.add(new ResourceEntry(next, inclusionType)); + } + } + } + + return retVal; + } + + private String determinePatientCompartmentSearchParameterName(Class theResourceType) { + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType); + Set searchParams = resourceDef.getSearchParamsForCompartmentName("Patient").stream() + .map(RuntimeSearchParam::getName) + .collect(Collectors.toSet()); + // A few we prefer + if (searchParams.contains(Observation.SP_PATIENT)) { + return Observation.SP_PATIENT; + } + if (searchParams.contains(Observation.SP_SUBJECT)) { + return Observation.SP_SUBJECT; + } + if (searchParams.contains(Coverage.SP_BENEFICIARY)) { + return Observation.SP_SUBJECT; + } + return searchParams.iterator().next(); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategy.java similarity index 69% rename from hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java rename to hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategy.java index 7992dfefa1e..5eb41ab6f4c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategy.java @@ -17,21 +17,12 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.ips.api; +package ca.uhn.fhir.jpa.ips.jpa; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class JpaSectionSearchStrategy implements IJpaSectionSearchStrategy { + + // nothing for now, interface has default methods -public enum IpsSectionEnum { - ALLERGY_INTOLERANCE, - MEDICATION_SUMMARY, - PROBLEM_LIST, - IMMUNIZATIONS, - PROCEDURES, - MEDICAL_DEVICES, - DIAGNOSTIC_RESULTS, - VITAL_SIGNS, - ILLNESS_HISTORY, - PREGNANCY, - SOCIAL_HISTORY, - FUNCTIONAL_STATUS, - PLAN_OF_CARE, - ADVANCE_DIRECTIVES } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java new file mode 100644 index 00000000000..e281ae31844 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java @@ -0,0 +1,62 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class JpaSectionSearchStrategyCollection { + + private Map, Object> mySearchStrategies; + + private JpaSectionSearchStrategyCollection(Map, Object> theSearchStrategies) { + mySearchStrategies = theSearchStrategies; + } + + @SuppressWarnings("unchecked") + public IJpaSectionSearchStrategy getSearchStrategy(Class theClass) { + return (IJpaSectionSearchStrategy) mySearchStrategies.get(theClass); + } + + public Collection> getResourceTypes() { + return mySearchStrategies.keySet(); + } + + public static JpaSectionSearchStrategyCollectionBuilder newBuilder() { + return new JpaSectionSearchStrategyCollectionBuilder(); + } + + public static class JpaSectionSearchStrategyCollectionBuilder { + private Map, Object> mySearchStrategies = new HashMap<>(); + + public JpaSectionSearchStrategyCollectionBuilder addStrategy( + Class theType, IJpaSectionSearchStrategy theSearchStrategy) { + mySearchStrategies.put(theType, theSearchStrategy); + return this; + } + + public JpaSectionSearchStrategyCollection build() { + return new JpaSectionSearchStrategyCollection(mySearchStrategies); + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..501f581b8c5 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java @@ -0,0 +1,41 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Consent; + +public class AdvanceDirectivesJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Consent.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + Consent.ConsentState.ACTIVE.getSystem(), Consent.ConsentState.ACTIVE.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..f485091d80c --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java @@ -0,0 +1,44 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.AllergyIntolerance; + +public class AllergyIntoleranceJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull AllergyIntolerance theCandidate) { + return !theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "inactive") + && !theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "resolved") + && !theCandidate + .getVerificationStatus() + .hasCoding( + "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification", + "entered-in-error"); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java new file mode 100644 index 00000000000..8425d7cdbe3 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java @@ -0,0 +1,42 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.DiagnosticReport; + +public class DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport + extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull DiagnosticReport theCandidate) { + if (theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.CANCELLED + || theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.ENTEREDINERROR + || theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java new file mode 100644 index 00000000000..bb77621cef1 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java @@ -0,0 +1,56 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +public class DiagnosticResultsJpaSectionSearchStrategyObservation extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CATEGORY, + new TokenOrListParam() + .addOr(new TokenParam( + "http://terminology.hl7.org/CodeSystem/observation-category", "laboratory"))); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + if (theCandidate.getStatus() == Observation.ObservationStatus.CANCELLED + || theCandidate.getStatus() == Observation.ObservationStatus.ENTEREDINERROR + || theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..6a5631dda0a --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java @@ -0,0 +1,40 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.ClinicalImpression; + +public class FunctionalStatusJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull ClinicalImpression theCandidate) { + if (theCandidate.getStatus() == ClinicalImpression.ClinicalImpressionStatus.INPROGRESS + || theCandidate.getStatus() == ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR) { + return false; + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..feebd978b7d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java @@ -0,0 +1,53 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Condition; + +public class IllnessHistoryJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Condition theCandidate) { + if (theCandidate + .getVerificationStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) { + return false; + } + + if (theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") + || theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") + || theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "remission")) { + return true; + } else { + return false; + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..7329bd038f0 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java @@ -0,0 +1,50 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Immunization; + +public class ImmunizationsJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC)); + theSearchParameterMap.addInclude(Immunization.INCLUDE_MANUFACTURER); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Immunization theCandidate) { + if (theCandidate.getStatus() == Immunization.ImmunizationStatus.ENTEREDINERROR) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..90c279a7414 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.DeviceUseStatement; + +public class MedicalDevicesJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(DeviceUseStatement.INCLUDE_DEVICE); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull DeviceUseStatement theCandidate) { + if (theCandidate.getStatus() == DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR) { + return false; + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java new file mode 100644 index 00000000000..3cec15b99e1 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationAdministration; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationAdministration + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationAdministration.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationAdministration.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationAdministration.MedicationAdministrationStatus.INPROGRESS.getSystem(), + MedicationAdministration.MedicationAdministrationStatus.INPROGRESS.toCode())) + .addOr(new TokenParam( + MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.getSystem(), + MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationAdministration.MedicationAdministrationStatus.ONHOLD.getSystem(), + MedicationAdministration.MedicationAdministrationStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java new file mode 100644 index 00000000000..fc476cf3b3f --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationDispense; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationDispense + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationDispense.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationDispense.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationDispense.MedicationDispenseStatus.INPROGRESS.getSystem(), + MedicationDispense.MedicationDispenseStatus.INPROGRESS.toCode())) + .addOr(new TokenParam( + MedicationDispense.MedicationDispenseStatus.UNKNOWN.getSystem(), + MedicationDispense.MedicationDispenseStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationDispense.MedicationDispenseStatus.ONHOLD.getSystem(), + MedicationDispense.MedicationDispenseStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java new file mode 100644 index 00000000000..3d51ed17d6b --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationRequest; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationRequest + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationRequest.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationRequest.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationRequest.MedicationRequestStatus.ACTIVE.getSystem(), + MedicationRequest.MedicationRequestStatus.ACTIVE.toCode())) + .addOr(new TokenParam( + MedicationRequest.MedicationRequestStatus.UNKNOWN.getSystem(), + MedicationRequest.MedicationRequestStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationRequest.MedicationRequestStatus.ONHOLD.getSystem(), + MedicationRequest.MedicationRequestStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java new file mode 100644 index 00000000000..8089b76d1aa --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java @@ -0,0 +1,54 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationStatement; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationStatement + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationStatement.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationStatement.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.ACTIVE.getSystem(), + MedicationStatement.MedicationStatementStatus.ACTIVE.toCode())) + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.INTENDED.getSystem(), + MedicationStatement.MedicationStatementStatus.INTENDED.toCode())) + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.UNKNOWN.getSystem(), + MedicationStatement.MedicationStatementStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.ONHOLD.getSystem(), + MedicationStatement.MedicationStatementStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..4656eae3136 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.CarePlan; + +public class PlanOfCareJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + CarePlan.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + CarePlan.CarePlanStatus.ACTIVE.getSystem(), CarePlan.CarePlanStatus.ACTIVE.toCode())) + .addOr(new TokenParam( + CarePlan.CarePlanStatus.ONHOLD.getSystem(), CarePlan.CarePlanStatus.ONHOLD.toCode())) + .addOr(new TokenParam( + CarePlan.CarePlanStatus.UNKNOWN.getSystem(), + CarePlan.CarePlanStatus.UNKNOWN.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..20e6ee5d35f --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java @@ -0,0 +1,75 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; + +public class PregnancyJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + public static final String LOINC_CODE_PREGNANCY_STATUS = "82810-3"; + public static final String LOINC_CODE_NUMBER_BIRTHS_LIVE = "11636-8"; + public static final String LOINC_CODE_NUMBER_BIRTHS_PRETERM = "11637-6"; + public static final String LOINC_CODE_NUMBER_BIRTHS_STILL_LIVING = "11638-4"; + public static final String LOINC_CODE_NUMBER_BIRTHS_TERM = "11639-2"; + public static final String LOINC_CODE_NUMBER_BIRTHS_TOTAL = "11640-0"; + public static final String LOINC_CODE_NUMBER_ABORTIONS = "11612-9"; + public static final String LOINC_CODE_NUMBER_ABORTIONS_INDUCED = "11613-7"; + public static final String LOINC_CODE_NUMBER_ABORTIONS_SPONTANEOUS = "11614-5"; + public static final String LOINC_CODE_NUMBER_ECTOPIC_PREGNANCY = "33065-4"; + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CODE, + new TokenOrListParam() + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_PREGNANCY_STATUS)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_LIVE)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_PRETERM)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_STILL_LIVING)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_TERM)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_TOTAL)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS_INDUCED)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS_SPONTANEOUS)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ECTOPIC_PREGNANCY))); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + if (theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..97e126049fc --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Condition; + +public class ProblemListJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Condition theCandidate) { + if (theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") + || theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") + || theCandidate + .getVerificationStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..3a76d79e6a8 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java @@ -0,0 +1,40 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Procedure; + +public class ProceduresJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Procedure theCandidate) { + if (theCandidate.getStatus() == Procedure.ProcedureStatus.ENTEREDINERROR + || theCandidate.getStatus() == Procedure.ProcedureStatus.NOTDONE) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..dc1fc63f6e9 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java @@ -0,0 +1,54 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +public class SocialHistoryJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CATEGORY, + new TokenOrListParam() + .addOr(new TokenParam( + "http://terminology.hl7.org/CodeSystem/observation-category", "social-history"))); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + if (theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..cee888cda26 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +public class VitalSignsJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CATEGORY, + new TokenOrListParam() + .addOr(new TokenParam( + "http://terminology.hl7.org/CodeSystem/observation-category", "vital-signs"))); + } + + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + return theCandidate.getStatus() != Observation.ObservationStatus.CANCELLED + && theCandidate.getStatus() != Observation.ObservationStatus.ENTEREDINERROR + && theCandidate.getStatus() != Observation.ObservationStatus.PRELIMINARY; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java index 7ee56683b49..b946a9e5222 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java @@ -28,8 +28,14 @@ import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ValidateUtil; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.thymeleaf.util.Validate; public class IpsOperationProvider { @@ -38,7 +44,8 @@ public class IpsOperationProvider { /** * Constructor */ - public IpsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) { + public IpsOperationProvider(@Nonnull IIpsGeneratorSvc theIpsGeneratorSvc) { + Validate.notNull(theIpsGeneratorSvc, "theIpsGeneratorSvc must not be null"); myIpsGeneratorSvc = theIpsGeneratorSvc; } @@ -54,9 +61,12 @@ public class IpsOperationProvider { bundleType = BundleTypeEnum.DOCUMENT, typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL) - public IBaseBundle patientInstanceSummary(@IdParam IIdType thePatientId, RequestDetails theRequestDetails) { - - return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId); + public IBaseBundle patientInstanceSummary( + @IdParam IIdType thePatientId, + @OperationParam(name = "profile", min = 0, typeName = "uri") IPrimitiveType theProfile, + RequestDetails theRequestDetails) { + String profile = theProfile != null ? theProfile.getValueAsString() : null; + return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId, profile); } /** @@ -72,12 +82,20 @@ public class IpsOperationProvider { typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL) public IBaseBundle patientTypeSummary( + @OperationParam(name = "profile", min = 0, typeName = "uri") IPrimitiveType theProfile, @Description( shortDefinition = "When the logical id of the patient is not used, servers MAY choose to support patient selection based on provided identifier") - @OperationParam(name = "identifier", min = 0, max = 1) - TokenParam thePatientIdentifier, + @OperationParam(name = "identifier", min = 1, max = 1, typeName = "Identifier") + IBase thePatientIdentifier, RequestDetails theRequestDetails) { - return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientIdentifier); + String profile = theProfile != null ? theProfile.getValueAsString() : null; + + ValidateUtil.isTrueOrThrowInvalidRequest(thePatientIdentifier != null, "No ID or identifier supplied"); + + FhirTerser terser = theRequestDetails.getFhirContext().newTerser(); + String system = terser.getSinglePrimitiveValueOrNull(thePatientIdentifier, "system"); + String value = terser.getSinglePrimitiveValueOrNull(thePatientIdentifier, "value"); + return myIpsGeneratorSvc.generateIps(theRequestDetails, new TokenParam(system, value), profile); } } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java new file mode 100644 index 00000000000..23d048e264c --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java @@ -0,0 +1,46 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Reference; + +public class AllergyIntoleranceNoInfoR4Generator implements INoInfoGenerator { + @Override + public IBaseResource generate(IIdType theSubjectId) { + AllergyIntolerance allergy = new AllergyIntolerance(); + allergy.setCode(new CodeableConcept() + .addCoding(new Coding() + .setCode("no-allergy-info") + .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") + .setDisplay("No information about allergies"))) + .setPatient(new Reference(theSubjectId)) + .setClinicalStatus(new CodeableConcept() + .addCoding(new Coding() + .setCode("active") + .setSystem("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"))); + return allergy; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java new file mode 100644 index 00000000000..12621839568 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java @@ -0,0 +1,130 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; +import ca.uhn.fhir.jpa.ips.api.IpsContext; +import ca.uhn.fhir.jpa.ips.api.Section; +import com.google.common.collect.Lists; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Organization; +import org.thymeleaf.util.Validate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings({"HttpUrlsUsage"}) +public abstract class BaseIpsGenerationStrategy implements IIpsGenerationStrategy { + + public static final String DEFAULT_IPS_NARRATIVES_PROPERTIES = + "classpath:ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties"; + private final List
mySections = new ArrayList<>(); + private final Map mySectionToResourceSupplier = new HashMap<>(); + + /** + * Constructor + */ + public BaseIpsGenerationStrategy() { + super(); + } + + @Override + public String getBundleProfile() { + return "http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips"; + } + + @Nonnull + @Override + public final List
getSections() { + return Collections.unmodifiableList(mySections); + } + + @Nonnull + @Override + public ISectionResourceSupplier getSectionResourceSupplier(@Nonnull Section theSection) { + return mySectionToResourceSupplier.get(theSection); + } + + /** + * This should be called once per section to add a section for inclusion in generated IPS documents. + * It should include a {@link Section} which contains static details about the section, and a {@link ISectionResourceSupplier} + * which is used to fetch resources for inclusion at runtime. + * + * @param theSection Contains static details about the section, such as the resource types it can contain, and a title. + * @param theSectionResourceSupplier The strategy object which will be used to supply content for this section at runtime. + */ + public void addSection(Section theSection, ISectionResourceSupplier theSectionResourceSupplier) { + Validate.notNull(theSection, "theSection must not be null"); + Validate.notNull(theSectionResourceSupplier, "theSectionResourceSupplier must not be null"); + Validate.isTrue( + !mySectionToResourceSupplier.containsKey(theSection), + "A section with the given profile already exists"); + + mySections.add(theSection); + mySectionToResourceSupplier.put(theSection, theSectionResourceSupplier); + } + + @Override + public List getNarrativePropertyFiles() { + return Lists.newArrayList(DEFAULT_IPS_NARRATIVES_PROPERTIES); + } + + @Override + public IBaseResource createAuthor() { + Organization organization = new Organization(); + organization + .setName("eHealthLab - University of Cyprus") + .addAddress(new Address() + .addLine("1 University Avenue") + .setCity("Nicosia") + .setPostalCode("2109") + .setCountry("CY")) + .setId(IdType.newRandomUuid()); + return organization; + } + + @Override + public String createTitle(IpsContext theContext) { + return "Patient Summary as of " + + DateTimeFormatter.ofPattern("MM/dd/yyyy").format(LocalDate.now()); + } + + @Override + public String createConfidentiality(IpsContext theIpsContext) { + return Composition.DocumentConfidentiality.N.toCode(); + } + + @Override + public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource) { + return null; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java deleted file mode 100644 index eccc26fd6f6..00000000000 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java +++ /dev/null @@ -1,446 +0,0 @@ -/*- - * #%L - * HAPI FHIR JPA Server - International Patient Summary (IPS) - * %% - * Copyright (C) 2014 - 2024 Smile CDR, Inc. - * %% - * 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% - */ -package ca.uhn.fhir.jpa.ips.strategy; - -import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; -import ca.uhn.fhir.jpa.ips.api.IpsContext; -import ca.uhn.fhir.jpa.ips.api.SectionRegistry; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.rest.api.SortOrderEnum; -import ca.uhn.fhir.rest.api.SortSpec; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; - -@SuppressWarnings({"EnhancedSwitchMigration", "HttpUrlsUsage"}) -public class DefaultIpsGenerationStrategy implements IIpsGenerationStrategy { - - public static final String DEFAULT_IPS_NARRATIVES_PROPERTIES = - "classpath:ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties"; - private SectionRegistry mySectionRegistry; - - /** - * Constructor - */ - public DefaultIpsGenerationStrategy() { - setSectionRegistry(new SectionRegistry()); - } - - @Override - public SectionRegistry getSectionRegistry() { - return mySectionRegistry; - } - - public void setSectionRegistry(SectionRegistry theSectionRegistry) { - if (!theSectionRegistry.isInitialized()) { - theSectionRegistry.initialize(); - } - mySectionRegistry = theSectionRegistry; - } - - @Override - public List getNarrativePropertyFiles() { - return Lists.newArrayList(DEFAULT_IPS_NARRATIVES_PROPERTIES); - } - - @Override - public IBaseResource createAuthor() { - Organization organization = new Organization(); - organization - .setName("eHealthLab - University of Cyprus") - .addAddress(new Address() - .addLine("1 University Avenue") - .setCity("Nicosia") - .setPostalCode("2109") - .setCountry("CY")) - .setId(IdType.newRandomUuid()); - return organization; - } - - @Override - public String createTitle(IpsContext theContext) { - return "Patient Summary as of " - + DateTimeFormatter.ofPattern("MM/dd/yyyy").format(LocalDate.now()); - } - - @Override - public String createConfidentiality(IpsContext theIpsContext) { - return Composition.DocumentConfidentiality.N.toCode(); - } - - @Override - public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource) { - return IdType.newRandomUuid(); - } - - @Override - public void massageResourceSearch( - IpsContext.IpsSectionContext theIpsSectionContext, SearchParameterMap theSearchParameterMap) { - switch (theIpsSectionContext.getSection()) { - case ALLERGY_INTOLERANCE: - case PROBLEM_LIST: - case PROCEDURES: - case MEDICAL_DEVICES: - case ILLNESS_HISTORY: - case FUNCTIONAL_STATUS: - return; - case IMMUNIZATIONS: - theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC)); - return; - case VITAL_SIGNS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CATEGORY, - new TokenOrListParam() - .addOr(new TokenParam( - "http://terminology.hl7.org/CodeSystem/observation-category", - "vital-signs"))); - return; - } - break; - case SOCIAL_HISTORY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CATEGORY, - new TokenOrListParam() - .addOr(new TokenParam( - "http://terminology.hl7.org/CodeSystem/observation-category", - "social-history"))); - return; - } - break; - case DIAGNOSTIC_RESULTS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) { - return; - } else if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CATEGORY, - new TokenOrListParam() - .addOr(new TokenParam( - "http://terminology.hl7.org/CodeSystem/observation-category", - "laboratory"))); - return; - } - break; - case PREGNANCY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CODE, - new TokenOrListParam() - .addOr(new TokenParam(LOINC_URI, "82810-3")) - .addOr(new TokenParam(LOINC_URI, "11636-8")) - .addOr(new TokenParam(LOINC_URI, "11637-6")) - .addOr(new TokenParam(LOINC_URI, "11638-4")) - .addOr(new TokenParam(LOINC_URI, "11639-2")) - .addOr(new TokenParam(LOINC_URI, "11640-0")) - .addOr(new TokenParam(LOINC_URI, "11612-9")) - .addOr(new TokenParam(LOINC_URI, "11613-7")) - .addOr(new TokenParam(LOINC_URI, "11614-5")) - .addOr(new TokenParam(LOINC_URI, "33065-4"))); - return; - } - break; - case MEDICATION_SUMMARY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationStatement.name())) { - theSearchParameterMap.add( - MedicationStatement.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.ACTIVE.getSystem(), - MedicationStatement.MedicationStatementStatus.ACTIVE.toCode())) - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.INTENDED.getSystem(), - MedicationStatement.MedicationStatementStatus.INTENDED.toCode())) - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.UNKNOWN.getSystem(), - MedicationStatement.MedicationStatementStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.ONHOLD.getSystem(), - MedicationStatement.MedicationStatementStatus.ONHOLD.toCode()))); - return; - } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationRequest.name())) { - theSearchParameterMap.add( - MedicationRequest.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationRequest.MedicationRequestStatus.ACTIVE.getSystem(), - MedicationRequest.MedicationRequestStatus.ACTIVE.toCode())) - .addOr(new TokenParam( - MedicationRequest.MedicationRequestStatus.UNKNOWN.getSystem(), - MedicationRequest.MedicationRequestStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationRequest.MedicationRequestStatus.ONHOLD.getSystem(), - MedicationRequest.MedicationRequestStatus.ONHOLD.toCode()))); - return; - } else if (theIpsSectionContext - .getResourceType() - .equals(ResourceType.MedicationAdministration.name())) { - theSearchParameterMap.add( - MedicationAdministration.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationAdministration.MedicationAdministrationStatus.INPROGRESS - .getSystem(), - MedicationAdministration.MedicationAdministrationStatus.INPROGRESS - .toCode())) - .addOr(new TokenParam( - MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.getSystem(), - MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationAdministration.MedicationAdministrationStatus.ONHOLD.getSystem(), - MedicationAdministration.MedicationAdministrationStatus.ONHOLD.toCode()))); - return; - } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationDispense.name())) { - theSearchParameterMap.add( - MedicationDispense.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationDispense.MedicationDispenseStatus.INPROGRESS.getSystem(), - MedicationDispense.MedicationDispenseStatus.INPROGRESS.toCode())) - .addOr(new TokenParam( - MedicationDispense.MedicationDispenseStatus.UNKNOWN.getSystem(), - MedicationDispense.MedicationDispenseStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationDispense.MedicationDispenseStatus.ONHOLD.getSystem(), - MedicationDispense.MedicationDispenseStatus.ONHOLD.toCode()))); - return; - } - break; - case PLAN_OF_CARE: - if (theIpsSectionContext.getResourceType().equals(ResourceType.CarePlan.name())) { - theSearchParameterMap.add( - CarePlan.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - CarePlan.CarePlanStatus.ACTIVE.getSystem(), - CarePlan.CarePlanStatus.ACTIVE.toCode())) - .addOr(new TokenParam( - CarePlan.CarePlanStatus.ONHOLD.getSystem(), - CarePlan.CarePlanStatus.ONHOLD.toCode())) - .addOr(new TokenParam( - CarePlan.CarePlanStatus.UNKNOWN.getSystem(), - CarePlan.CarePlanStatus.UNKNOWN.toCode()))); - return; - } - break; - case ADVANCE_DIRECTIVES: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Consent.name())) { - theSearchParameterMap.add( - Consent.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - Consent.ConsentState.ACTIVE.getSystem(), - Consent.ConsentState.ACTIVE.toCode()))); - return; - } - break; - } - - // Shouldn't happen: This means none of the above switches handled the Section+resourceType combination - assert false - : "Don't know how to handle " + theIpsSectionContext.getSection() + "/" - + theIpsSectionContext.getResourceType(); - } - - @Nonnull - @Override - public Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext) { - switch (theIpsSectionContext.getSection()) { - case MEDICATION_SUMMARY: - if (ResourceType.MedicationStatement.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationStatement.INCLUDE_MEDICATION); - } - if (ResourceType.MedicationRequest.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationRequest.INCLUDE_MEDICATION); - } - if (ResourceType.MedicationAdministration.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationAdministration.INCLUDE_MEDICATION); - } - if (ResourceType.MedicationDispense.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationDispense.INCLUDE_MEDICATION); - } - break; - case MEDICAL_DEVICES: - if (ResourceType.DeviceUseStatement.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(DeviceUseStatement.INCLUDE_DEVICE); - } - break; - case IMMUNIZATIONS: - if (ResourceType.Immunization.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(Immunization.INCLUDE_MANUFACTURER); - } - break; - case ALLERGY_INTOLERANCE: - case PROBLEM_LIST: - case PROCEDURES: - case DIAGNOSTIC_RESULTS: - case VITAL_SIGNS: - case ILLNESS_HISTORY: - case PREGNANCY: - case SOCIAL_HISTORY: - case FUNCTIONAL_STATUS: - case PLAN_OF_CARE: - case ADVANCE_DIRECTIVES: - break; - } - return Collections.emptySet(); - } - - @SuppressWarnings("EnhancedSwitchMigration") - @Override - public boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate) { - - switch (theIpsSectionContext.getSection()) { - case MEDICATION_SUMMARY: - case PLAN_OF_CARE: - case ADVANCE_DIRECTIVES: - return true; - case ALLERGY_INTOLERANCE: - if (theIpsSectionContext.getResourceType().equals(ResourceType.AllergyIntolerance.name())) { - AllergyIntolerance allergyIntolerance = (AllergyIntolerance) theCandidate; - return !allergyIntolerance - .getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "inactive") - && !allergyIntolerance - .getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "resolved") - && !allergyIntolerance - .getVerificationStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification", - "entered-in-error"); - } - break; - case PROBLEM_LIST: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) { - Condition prob = (Condition) theCandidate; - return !prob.getClinicalStatus() - .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") - && !prob.getClinicalStatus() - .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") - && !prob.getVerificationStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-ver-status", - "entered-in-error"); - } - break; - case IMMUNIZATIONS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Immunization.name())) { - Immunization immunization = (Immunization) theCandidate; - return immunization.getStatus() != Immunization.ImmunizationStatus.ENTEREDINERROR; - } - break; - case PROCEDURES: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Procedure.name())) { - Procedure proc = (Procedure) theCandidate; - return proc.getStatus() != Procedure.ProcedureStatus.ENTEREDINERROR - && proc.getStatus() != Procedure.ProcedureStatus.NOTDONE; - } - break; - case MEDICAL_DEVICES: - if (theIpsSectionContext.getResourceType().equals(ResourceType.DeviceUseStatement.name())) { - DeviceUseStatement deviceUseStatement = (DeviceUseStatement) theCandidate; - return deviceUseStatement.getStatus() != DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR; - } - return true; - case DIAGNOSTIC_RESULTS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) { - return true; - } - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case VITAL_SIGNS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case ILLNESS_HISTORY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) { - Condition prob = (Condition) theCandidate; - if (prob.getVerificationStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) { - return false; - } else { - return prob.getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") - || prob.getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") - || prob.getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-clinical", - "remission"); - } - } - break; - case PREGNANCY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case SOCIAL_HISTORY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case FUNCTIONAL_STATUS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.ClinicalImpression.name())) { - ClinicalImpression clinicalImpression = (ClinicalImpression) theCandidate; - return clinicalImpression.getStatus() != ClinicalImpression.ClinicalImpressionStatus.INPROGRESS - && clinicalImpression.getStatus() - != ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR; - } - break; - } - - return true; - } -} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java new file mode 100644 index 00000000000..9d6861a160d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java @@ -0,0 +1,45 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.MedicationStatement; +import org.hl7.fhir.r4.model.Reference; + +public class MedicationNoInfoR4Generator implements INoInfoGenerator { + @Override + public IBaseResource generate(IIdType theSubjectId) { + MedicationStatement medication = new MedicationStatement(); + // setMedicationCodeableConcept is not available + medication + .setMedication(new CodeableConcept() + .addCoding(new Coding() + .setCode("no-medication-info") + .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") + .setDisplay("No information about medications"))) + .setSubject(new Reference(theSubjectId)) + .setStatus(MedicationStatement.MedicationStatementStatus.UNKNOWN); + return medication; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java new file mode 100644 index 00000000000..4d69eb1312a --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Reference; + +public class ProblemNoInfoR4Generator implements INoInfoGenerator { + @Override + public IBaseResource generate(IIdType theSubjectId) { + Condition condition = new Condition(); + condition + .setCode(new CodeableConcept() + .addCoding(new Coding() + .setCode("no-problem-info") + .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") + .setDisplay("No information about problems"))) + .setSubject(new Reference(theSubjectId)) + .setClinicalStatus(new CodeableConcept() + .addCoding(new Coding() + .setCode("active") + .setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical"))); + return condition; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html index c9bfed14888..1855612b60b 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html @@ -6,8 +6,8 @@ Action Controlled: Consent.provision.action[x].{ text || coding[x].display (sepa Date: Consent.dateTime */-->
+
Advance Directives
- @@ -21,9 +21,9 @@ Date: Consent.dateTime - + - + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html index 5036bfc713d..728afbbd7a0 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html @@ -8,8 +8,8 @@ Severity: AllergyIntolerance.reaction.severity[x].code (separated by
) Comments: AllergyIntolerance.note[x].text (separated by
) */-->
+
Allergies And Intolerances
Advance Directives
Scope
ScopeScope StatusAction ControlledAction Controlled Date
- @@ -27,10 +27,10 @@ Comments: AllergyIntolerance.note[x].text (separated by
) - - - - + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html index 79c6015430a..560007cd56d 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html @@ -14,61 +14,75 @@ Code: DiagnosticReport.code.text || DiagnosticReport.code.coding[x].display (sep Date: DiagnosticReport.effectiveDateTime || DiagnosticReport.effectivePeriod.start */-->
-
Allergies And Intolerances
Allergen
Allergen StatusCategoryReactionSeverityCommentsCategoryReactionSeverityComments Onset
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Diagnostic Results: Observations
CodeResultUnitInterpretationReference RangeCommentsDate
CodeResultUnitInterpretationReference RangeCommentsDate
+ +
Diagnostic Results: Observations
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeResultUnitInterpretationReference RangeCommentsDate
Code + ResultUnit + Interpretation + + Reference + Range + CommentsDate
+
- - - - - - - - - - - - - - - - - - - - - - -
Diagnostic Results: Diagnostic Reports
CodeDate
DeviceDate
+ +
Diagnostic Results: Diagnostic Reports
+ + + + + + + + + + + + + + + + + + + + + +
CodeDate
Device + Date
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html index 30addeade65..a569efadf7b 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html @@ -7,8 +7,8 @@ Comments: ClinicalImpression.note[x].text (separated by
) Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod.start */-->
+
Functional Status
- @@ -23,11 +23,11 @@ Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html index 6e1aa367549..9e4bfcc6a96 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html @@ -5,8 +5,8 @@ Comments: Procedure.note[x].text(separated by
) Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“ && Procedure.performedPeriod.end || Procedure.performedAge || Procedure.performedRange.low && “-“ && Procedure.performedRange.high || Procedure.performedString */-->
+
History Of Procedures
Functional Status
Assessment
AssessmentAssessment Status FindingCommentsDateCommentsDate
- @@ -19,9 +19,9 @@ Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“ - - - + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html index 2386d815b38..e21ca2e1ae2 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html @@ -9,8 +9,8 @@ Comments: Immunization.note[x].text (separated by
) Date: Immunization.occurrenceDateTime || Immunization.occurrenceString */-->
+
Immunizations
History Of Procedures
Procedure
ProcedureCommentsDateProcedureCommentsDate
- @@ -27,13 +27,13 @@ Date: Immunization.occurrenceDateTime || Immunization.occurrenceString - + - - + + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html index fe23a21d5fd..73902b88bab 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html @@ -6,8 +6,8 @@ Comments: DeviceUseStatement.note[x].text (separated by
) Date Recorded: DeviceUseStatement.recordedDateTime */-->
+
Medical Devices
Immunizations
Immunization
ImmunizationImmunization StatusCommentsManufacturerCommentsManufacturer Lot NumberCommentsDateCommentsDate
- @@ -21,10 +21,10 @@ Date Recorded: DeviceUseStatement.recordedDateTime - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html index ade250ed8ee..ed4a8f128d7 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html @@ -16,63 +16,78 @@ Sig: MedicationStatement.dosage[x].text (display all sigs separated by
) Date: MedicationStatement.effectiveDateTime || MedicationStatement.effectivePeriod.start */-->
-
Medical Devices
Device
DeviceDevice StatusCommentsDate RecordedCommentsDate Recorded
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Medication Summary: Medication Requests
MedicationStatusRouteSigCommentsAuthored Date
MedicationStatusRouteSigCommentsAuthored Date
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Medication Summary: Medication Statements
MedicationStatusRouteSigDate
MedicationStatusRouteSigDate
+ +
Medication Summary: Medication Requests
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MedicationStatusRouteSigCommentsAuthored Date
+ Medication + Status + Route + + Sig + CommentsAuthored Date
+
+ + +
Medication Summary: Medication Statements
+ + + + + + + + + + + + + + + + + + + + + + + + + +
MedicationStatusRouteSigDate
+ Medication + StatusRouteSigDate
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html index 0f3223999f7..11f06d54bf5 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html @@ -6,8 +6,8 @@ Comments: Condition.note[x].text (separated by
) Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString */-->
+
Past History of Illnesses
- @@ -21,10 +21,10 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html index 3b43317f4d3..d87170fac24 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html @@ -7,8 +7,8 @@ Planned Start: CarePlan.period.start Planned End: CarePlan.period.end */-->
+
Plan of Care
Past History of Illnesses
Medical Problems
Medical ProblemMedical Problem StatusCommentsOnset DateCommentsOnset Date
- @@ -25,7 +25,7 @@ Planned End: CarePlan.period.end - + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html index b4fd5a3a3ae..d939053c3eb 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html @@ -6,8 +6,8 @@ Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */-->
+
Pregnancy
Plan of Care
Activity
Activity IntentCommentsComments Planned Start Planned End
- @@ -21,10 +21,10 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start - - - - + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html index 02a6ab4ed76..5c0e796bd5c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html @@ -6,8 +6,8 @@ Comments: Condition.note[x].text (separated by
) Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString */-->
+
Problem List
Pregnancy
Code
CodeResultCommentsDateCodeResultCommentsDate
- @@ -21,10 +21,10 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html index 9472f17f9b4..de7ba86c8b8 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html @@ -7,8 +7,8 @@ Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */-->
+
Social History
Problem List
Medical Problems
Medical ProblemsMedical Problems StatusCommentsOnset DateCommentsOnset Date
- @@ -23,11 +23,11 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start - - - - - + + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html index 89c01826383..f890ad6c26e 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html @@ -8,8 +8,8 @@ Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */-->
+
Vital Signs
Social History
Code
CodeResultUnitCommentsDateCodeResultUnitCommentsDate
- @@ -25,12 +25,12 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start - - - - - - + + + + + + diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java index d075f0d138d..1a28350edce 100644 --- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java @@ -7,14 +7,17 @@ import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider; -import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.util.ClasspathUtil; -import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -38,9 +41,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; @@ -49,9 +51,9 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; /** * This test uses a complete R4 JPA server as a backend and wires the @@ -99,8 +101,8 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { // Verify validateDocument(output); assertEquals(117, output.getEntry().size()); - String patientId = findFirstEntryResource(output, Patient.class, 1).getId(); - assertThat(patientId, matchesPattern("urn:uuid:.*")); + String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue(); + assertEquals("Patient/f15d2419-fbff-464a-826d-0afe8f095771", patientId); MedicationStatement medicationStatement = findFirstEntryResource(output, MedicationStatement.class, 2); assertEquals(patientId, medicationStatement.getSubject().getReference()); assertNull(medicationStatement.getInformationSource().getReference()); @@ -186,8 +188,8 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { // Verify validateDocument(output); assertEquals(7, output.getEntry().size()); - String patientId = findFirstEntryResource(output, Patient.class, 1).getId(); - assertThat(patientId, matchesPattern("urn:uuid:.*")); + String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue(); + assertEquals("Patient/5342998", patientId); assertEquals(patientId, findEntryResource(output, Condition.class, 0, 2).getSubject().getReference()); assertEquals(patientId, findEntryResource(output, Condition.class, 1, 2).getSubject().getReference()); @@ -280,18 +282,9 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { instanceValidator.setValidationSupport(new ValidationSupportChain(new IpsTerminologySvc(), myFhirContext.getValidationSupport())); validator.registerValidatorModule(instanceValidator); ValidationResult validation = validator.validateWithResult(theOutcome); - assertTrue(validation.isSuccessful(), () -> myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(validation.toOperationOutcome())); - // Make sure that all refs have been replaced with UUIDs - List references = myFhirContext.newTerser().getAllResourceReferences(theOutcome); - for (IBaseResource next : myFhirContext.newTerser().getAllEmbeddedResources(theOutcome, true)) { - references.addAll(myFhirContext.newTerser().getAllResourceReferences(next)); - } - for (ResourceReferenceInfo next : references) { - if (!next.getResourceReference().getReferenceElement().getValue().startsWith("urn:uuid:")) { - fail(next.getName()); - } - } + Optional failure = validation.getMessages().stream().filter(t -> t.getSeverity().ordinal() >= ResultSeverityEnum.ERROR.ordinal()).findFirst(); + assertFalse(failure.isPresent(), () -> failure.get().toString()); } @Configuration @@ -299,12 +292,12 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { @Bean public IIpsGenerationStrategy ipsGenerationStrategy() { - return new DefaultIpsGenerationStrategy(); + return new DefaultJpaIpsGenerationStrategy(); } @Bean public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { - return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry); + return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy); } @Bean @@ -315,7 +308,6 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { } - @SuppressWarnings("unchecked") private static T findFirstEntryResource(Bundle theBundle, Class theType, int theExpectedCount) { return findEntryResource(theBundle, theType, 0, theExpectedCount); } diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java index 0218697e19c..39278765825 100644 --- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java @@ -3,9 +3,10 @@ package ca.uhn.fhir.jpa.ips.generator; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.ips.api.IpsSectionEnum; -import ca.uhn.fhir.jpa.ips.api.SectionRegistry; -import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.IpsContext; +import ca.uhn.fhir.jpa.ips.api.Section; +import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -15,13 +16,11 @@ import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.test.utilities.HtmlUtil; import ca.uhn.fhir.util.ClasspathUtil; -import org.htmlunit.html.DomElement; -import org.htmlunit.html.DomNodeList; -import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlTable; -import org.htmlunit.html.HtmlTableRow; import com.google.common.collect.Lists; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.AllergyIntolerance; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CarePlan; @@ -61,11 +60,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.annotation.Nonnull; import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.ips.generator.IpsGenerationR4Test.findEntryResource; @@ -76,6 +75,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -115,23 +115,44 @@ public class IpsGeneratorSvcImplTest { private final FhirContext myFhirContext = FhirContext.forR4Cached(); private final DaoRegistry myDaoRegistry = new DaoRegistry(myFhirContext); private IIpsGeneratorSvc mySvc; - private DefaultIpsGenerationStrategy myStrategy; + private DefaultJpaIpsGenerationStrategy myStrategy; @BeforeEach public void beforeEach() { myDaoRegistry.setResourceDaos(Collections.emptyList()); + } - myStrategy = new DefaultIpsGenerationStrategy(); - mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy, myDaoRegistry); + private void initializeGenerationStrategy() { + initializeGenerationStrategy(List.of()); + } + + private void initializeGenerationStrategy(List> theGlobalSectionCustomizers) { + myStrategy = new DefaultJpaIpsGenerationStrategy() { + @Override + public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @javax.annotation.Nonnull IBaseResource theResource) { + return IdType.newRandomUuid(); + } + }; + + myStrategy.setFhirContext(myFhirContext); + myStrategy.setDaoRegistry(myDaoRegistry); + + if (theGlobalSectionCustomizers != null) { + for (var next : theGlobalSectionCustomizers) { + myStrategy.addGlobalSectionCustomizer(next); + } + } + mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy); } @Test public void testGenerateIps() { // Setup + initializeGenerationStrategy(); registerResourceDaosForSmallPatientSet(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar")); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar"), null); // Verify ourLog.info("Generated IPS:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); @@ -168,6 +189,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testAllergyIntolerance_OnsetTypes() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); AllergyIntolerance allergy1 = new AllergyIntolerance(); @@ -194,11 +216,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ALLERGY_INTOLERANCE); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -216,6 +238,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testAllergyIntolerance_MissingElements() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); AllergyIntolerance allergy = new AllergyIntolerance(); @@ -229,11 +252,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ALLERGY_INTOLERANCE); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -245,6 +268,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testMedicationSummary_MedicationStatementWithMedicationReference() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -256,7 +280,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Bundle Contents List contentResourceTypes = toEntryResourceTypeStrings(outcome); @@ -269,14 +293,14 @@ public class IpsGeneratorSvcImplTest { // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); DomNodeList tables = narrativeHtml.getElementsByTagName("table"); - assertEquals(2, tables.size()); - HtmlTable table = (HtmlTable) tables.get(1); + assertEquals(1, tables.size()); + HtmlTable table = (HtmlTable) tables.get(0); HtmlTableRow row = table.getBodies().get(0).getRows().get(0); assertEquals("Tylenol", row.getCell(0).asNormalizedText()); assertEquals("Active", row.getCell(1).asNormalizedText()); @@ -288,6 +312,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testMedicationSummary_MedicationRequestWithNoMedication() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -301,17 +326,17 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); DomNodeList tables = narrativeHtml.getElementsByTagName("table"); - assertEquals(2, tables.size()); + assertEquals(1, tables.size()); HtmlTable table = (HtmlTable) tables.get(0); HtmlTableRow row = table.getBodies().get(0).getRows().get(0); assertEquals("", row.getCell(0).asNormalizedText()); @@ -320,22 +345,12 @@ public class IpsGeneratorSvcImplTest { assertEquals("", row.getCell(3).asNormalizedText()); } - @Nonnull - private Composition.SectionComponent findSection(Composition compositions, IpsSectionEnum sectionEnum) { - Composition.SectionComponent section = compositions - .getSection() - .stream() - .filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(sectionEnum).getTitle())) - .findFirst() - .orElseThrow(); - return section; - } - @Test public void testMedicationSummary_DuplicateSecondaryResources() { - myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null))); - // Setup Patient + initializeGenerationStrategy( + List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build()) + ); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -349,7 +364,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Bundle Contents List contentResourceTypes = toEntryResourceTypeStrings(outcome); @@ -370,9 +385,10 @@ public class IpsGeneratorSvcImplTest { */ @Test public void testMedicationSummary_ResourceAppearsAsSecondaryThenPrimary() throws IOException { - myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null))); - // Setup Patient + initializeGenerationStrategy( + List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build()) + ); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -388,7 +404,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Bundle Contents List contentResourceTypes = toEntryResourceTypeStrings(outcome); @@ -403,20 +419,61 @@ public class IpsGeneratorSvcImplTest { // Verify narrative - should have 2 rows (one for each primary MedicationStatement) Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); DomNodeList tables = narrativeHtml.getElementsByTagName("table"); - assertEquals(2, tables.size()); - HtmlTable table = (HtmlTable) tables.get(1); + assertEquals(1, tables.size()); + HtmlTable table = (HtmlTable) tables.get(0); + assertEquals(2, table.getBodies().get(0).getRows().size()); + } + + /** + * If there is no contents in one of the 2 medication summary tables it should be + * omitted + */ + @Test + public void testMedicationSummary_OmitMedicationRequestTable() throws IOException { + // Setup Patient + initializeGenerationStrategy( + List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build()) + ); + registerPatientDaoWithRead(); + + // Setup Medication + MedicationStatement + Medication medication = createSecondaryMedication(MEDICATION_ID); + MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID); + medicationStatement.addDerivedFrom().setReference(MEDICATION_STATEMENT_ID2); + MedicationStatement medicationStatement2 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement2, BundleEntrySearchModeEnum.INCLUDE); + MedicationStatement medicationStatement3 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2); + IFhirResourceDao medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class); + when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication, medicationStatement2, medicationStatement3))); + + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); + + // Verify narrative - should have 2 rows (one for each primary MedicationStatement) + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); + + HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); + ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); + + DomNodeList tables = narrativeHtml.getElementsByTagName("table"); + assertEquals(1, tables.size()); + HtmlTable table = (HtmlTable) tables.get(0); assertEquals(2, table.getBodies().get(0).getRows().size()); } @Test public void testMedicalDevices_DeviceUseStatementWithDevice() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -439,11 +496,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICAL_DEVICES); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICAL_DEVICES); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -460,6 +517,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testImmunizations() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -486,11 +544,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.IMMUNIZATIONS); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_IMMUNIZATIONS); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -511,6 +569,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testReferencesUpdatedInSecondaryInclusions() { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -548,7 +607,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); // Verify cross-references @@ -572,10 +631,10 @@ public class IpsGeneratorSvcImplTest { ourLog.info("Resource: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); verify(conditionDao, times(2)).search(any(), any()); Composition composition = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent problemListSection = findSection(composition, IpsSectionEnum.PROBLEM_LIST); + Composition.SectionComponent problemListSection = findSection(composition, DefaultJpaIpsGenerationStrategy.SECTION_CODE_PROBLEM_LIST); assertEquals(addedCondition.getId(), problemListSection.getEntry().get(0).getReference()); assertEquals(1, problemListSection.getEntry().size()); - Composition.SectionComponent illnessHistorySection = findSection(composition, IpsSectionEnum.ILLNESS_HISTORY); + Composition.SectionComponent illnessHistorySection = findSection(composition, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ILLNESS_HISTORY); assertEquals(addedCondition2.getId(), illnessHistorySection.getEntry().get(0).getReference()); assertEquals(1, illnessHistorySection.getEntry().size()); } @@ -583,6 +642,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testPatientIsReturnedAsAnIncludeResource() { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Condition @@ -608,7 +668,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); List resources = outcome .getEntry() @@ -620,6 +680,30 @@ public class IpsGeneratorSvcImplTest { )); } + @Test + public void testSelectGenerator() { + IIpsGenerationStrategy strategy1 = mock(IIpsGenerationStrategy.class); + when(strategy1.getBundleProfile()).thenReturn("http://1"); + IIpsGenerationStrategy strategy2 = mock(IIpsGenerationStrategy.class); + when(strategy2.getBundleProfile()).thenReturn("http://2"); + IpsGeneratorSvcImpl svc = new IpsGeneratorSvcImpl(myFhirContext, List.of(strategy1, strategy2)); + + assertSame(strategy1, svc.selectGenerationStrategy("http://1")); + assertSame(strategy1, svc.selectGenerationStrategy(null)); + assertSame(strategy1, svc.selectGenerationStrategy("http://foo")); + assertSame(strategy2, svc.selectGenerationStrategy("http://2")); + } + + @Nonnull + private Composition.SectionComponent findSection(Composition compositions, String theSectionCode) { + return compositions + .getSection() + .stream() + .filter(t -> t.getCode().getCodingFirstRep().getCode().equals(theSectionCode)) + .findFirst() + .orElseThrow(); + } + private void registerPatientDaoWithRead() { IFhirResourceDao patientDao = registerResourceDaoWithNoData(Patient.class); Patient patient = new Patient(); @@ -677,19 +761,19 @@ public class IpsGeneratorSvcImplTest { } @Nonnull - private static Medication createSecondaryMedication(String medicationId) { + private static Medication createSecondaryMedication(String theMedicationId) { Medication medication = new Medication(); - medication.setId(new IdType(medicationId)); + medication.setId(new IdType(theMedicationId)); medication.getCode().addCoding().setDisplay("Tylenol"); ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE); return medication; } @Nonnull - private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) { + private static MedicationStatement createPrimaryMedicationStatement(String theMedicationId, String medicationStatementId) { MedicationStatement medicationStatement = new MedicationStatement(); medicationStatement.setId(medicationStatementId); - medicationStatement.setMedication(new Reference(medicationId)); + medicationStatement.setMedication(new Reference(theMedicationId)); medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE); medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral"); medicationStatement.getDosageFirstRep().setText("DAW"); diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java new file mode 100644 index 00000000000..96e6bd1c542 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java @@ -0,0 +1,152 @@ +package ca.uhn.fhir.jpa.ips.provider; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.UriType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class IpsOperationProviderTest { + + @Mock + private IIpsGeneratorSvc myIpsGeneratorSvc; + + @RegisterExtension + private RestfulServerExtension myServer = new RestfulServerExtension(FhirContext.forR4Cached()) + .withServer(t -> t.registerProviders(new IpsOperationProvider(myIpsGeneratorSvc))); + + @Captor + private ArgumentCaptor myProfileCaptor; + @Captor + private ArgumentCaptor myIdTypeCaptor; + @Captor + private ArgumentCaptor myTokenCaptor; + + @Test + public void testGenerateById() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(IIdType.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onInstance(new IdType("Patient/123")) + .named("$summary") + .withNoParameters(Parameters.class) + .returnResourceType(Bundle.class) + .execute(); + + // verify + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myIdTypeCaptor.capture(), myProfileCaptor.capture()); + assertEquals("Patient/123", myIdTypeCaptor.getValue().getValue()); + assertEquals(null, myProfileCaptor.getValue()); + } + + @Test + public void testGenerateById_WithProfile() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(IIdType.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onInstance(new IdType("Patient/123")) + .named("$summary") + .withParameter(Parameters.class, "profile", new UriType("http://foo")) + .returnResourceType(Bundle.class) + .execute(); + + // verify + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myIdTypeCaptor.capture(), myProfileCaptor.capture()); + assertEquals("Patient/123", myIdTypeCaptor.getValue().getValue()); + assertEquals("http://foo", myProfileCaptor.getValue()); + } + + @Test + public void testGenerateByIdentifier() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(TokenParam.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onType("Patient") + .named("$summary") + .withParameter(Parameters.class, "identifier", new Identifier().setSystem("http://system").setValue("value")) + .returnResourceType(Bundle.class) + .execute(); + + // verify + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myTokenCaptor.capture(), myProfileCaptor.capture()); + assertEquals("http://system", myTokenCaptor.getValue().getSystem()); + assertEquals("value", myTokenCaptor.getValue().getValue()); + assertEquals(null, myProfileCaptor.getValue()); + } + + @Test + public void testGenerateByIdentifier_WithProfile() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(TokenParam.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onType("Patient") + .named("$summary") + .withParameter(Parameters.class, "identifier", new Identifier().setSystem("http://system").setValue("value")) + .andParameter("profile", new UriType("http://foo")) + .returnResourceType(Bundle.class) + .execute(); + + // verify + + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myTokenCaptor.capture(), myProfileCaptor.capture()); + assertEquals("http://system", myTokenCaptor.getValue().getSystem()); + assertEquals("value", myTokenCaptor.getValue().getValue()); + assertEquals("http://foo", myProfileCaptor.getValue()); + } + +} diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 88bff1548cc..cf0f5377baa 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java index 38e7e4eb1a2..0ccaf493fc9 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java @@ -25,7 +25,6 @@ import ca.uhn.fhir.context.FhirContext; 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.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; @@ -184,7 +183,8 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc { MdmTransactionContext theMdmTransactionContext, RequestDetails theRequestDetails) { RequestPartitionId theReadPartitionId = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null); + myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.MDM_QUERY_LINKS); Page resultPage; if (theReadPartitionId.hasPartitionIds()) { theMdmQuerySearchParameters.setPartitionIds(theReadPartitionId.getPartitionIds()); @@ -242,7 +242,8 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc { String theRequestResourceType) { Page resultPage; RequestPartitionId readPartitionId = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, null); + myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.MDM_DUPLICATE_GOLDEN_RESOURCES); if (readPartitionId.isAllPartitions()) { resultPage = myMdmLinkQuerySvc.getDuplicateGoldenResources( @@ -318,10 +319,9 @@ public class MdmControllerSvcImpl implements IMdmControllerSvc { params.setBatchSize(theBatchSize.getValue().intValue()); } - ReadPartitionIdRequestDetails details = - ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_MDM_CLEAR); RequestPartitionId requestPartition = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details); + myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.OPERATION_MDM_CLEAR); params.setRequestPartitionId(requestPartition); JobInstanceStartRequest request = new JobInstanceStartRequest(); diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java index e51949689c4..10e903a301c 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java @@ -38,6 +38,8 @@ public class MdmResourceDaoSvcTest extends BaseMdmR4Test { @Autowired private ISearchParamExtractor mySearchParamExtractor; + private PatientIdPartitionInterceptor myPatientIdPartitionInterceptor; + @Override @AfterEach public void after() throws IOException { @@ -104,9 +106,8 @@ public class MdmResourceDaoSvcTest extends BaseMdmR4Test { myPartitionSettings.setPartitioningEnabled(true); myPartitionSettings.setUnnamedPartitionMode(true); myPartitionSettings.setIncludePartitionInSearchHashes(false); - - PatientIdPartitionInterceptor interceptor = new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings); - myInterceptorRegistry.registerInterceptor(interceptor); + myPatientIdPartitionInterceptor = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); + myInterceptorRegistry.registerInterceptor(myPatientIdPartitionInterceptor); try { StringOrListParam patientIds = new StringOrListParam(); @@ -154,7 +155,7 @@ public class MdmResourceDaoSvcTest extends BaseMdmR4Test { Patient patient = (Patient) result.getAllResources().get(0); assertTrue(patient.getId().contains(firstId.getValue())); } finally { - myInterceptorRegistry.unregisterInterceptor(interceptor); + myInterceptorRegistry.unregisterInterceptor(myPatientIdPartitionInterceptor); } } diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 98f3f16b884..073e7b6715f 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml 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 d9199787b72..49ad436112c 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 @@ -32,8 +32,6 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; -import static org.apache.commons.lang3.StringUtils.defaultString; - @Entity @Table( name = ResourceHistoryTable.HFJ_RES_VER, @@ -86,15 +84,12 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl @OneToMany(mappedBy = "myResourceHistory", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) private Collection myTags; - /** - * Note: No setter for this field because it's only a legacy way of storing data now. - */ @Column(name = "RES_TEXT", length = Integer.MAX_VALUE - 1, nullable = true) @Lob() @OptimisticLock(excluded = true) private byte[] myResource; - @Column(name = "RES_TEXT_VC", nullable = true, length = Length.LONG32) + @Column(name = "RES_TEXT_VC", length = Length.LONG32, nullable = true) @OptimisticLock(excluded = true) private String myResourceTextVc; @@ -155,8 +150,7 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl } public void setResourceTextVc(String theResourceTextVc) { - myResource = null; - myResourceTextVc = defaultString(theResourceTextVc); + myResourceTextVc = theResourceTextVc; } public ResourceHistoryProvenanceEntity getProvenance() { @@ -212,6 +206,10 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl return myResource; } + public void setResource(byte[] theResource) { + myResource = theResource; + } + @Override public Long getResourceId() { return myResourceId; diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index dfb03772e7d..2106b74afb8 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/interceptor/model/ReadPartitionIdRequestDetails.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/interceptor/model/ReadPartitionIdRequestDetails.java index 4d3e61d9fce..36218fd388f 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/interceptor/model/ReadPartitionIdRequestDetails.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/interceptor/model/ReadPartitionIdRequestDetails.java @@ -108,18 +108,9 @@ public class ReadPartitionIdRequestDetails extends PartitionIdRequestDetails { return forRead(theId.getResourceType(), theId, false); } - public static ReadPartitionIdRequestDetails forOperation( - @Nullable String theResourceType, @Nullable IIdType theId, @Nonnull String theExtendedOperationName) { - RestOperationTypeEnum op; - if (theId != null) { - op = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; - } else if (theResourceType != null) { - op = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; - } else { - op = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; - } - - return new ReadPartitionIdRequestDetails(theResourceType, op, null, null, null, null, theExtendedOperationName); + public static ReadPartitionIdRequestDetails forServerOperation(@Nonnull String theOperationName) { + return new ReadPartitionIdRequestDetails( + null, RestOperationTypeEnum.EXTENDED_OPERATION_SERVER, null, null, null, null, theOperationName); } public static ReadPartitionIdRequestDetails forRead( @@ -135,7 +126,7 @@ public class ReadPartitionIdRequestDetails extends PartitionIdRequestDetails { theResourceType, RestOperationTypeEnum.SEARCH_TYPE, null, - theParams, + theParams != null ? theParams : SearchParameterMap.newSynchronous(), theConditionalOperationTargetOrNull, null, null); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeListenerCacheRefresher.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeListenerCacheRefresher.java index aa9c72739f8..97cf3f7ea95 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeListenerCacheRefresher.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeListenerCacheRefresher.java @@ -19,6 +19,10 @@ */ package ca.uhn.fhir.jpa.cache; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; + /** * This is an internal service and is not intended to be used outside this package. Implementers should only directly * call the {@link IResourceChangeListenerRegistry}. @@ -40,4 +44,10 @@ public interface IResourceChangeListenerCacheRefresher { * @return the number of resources that have been created, updated and deleted since the last time the cache was refreshed */ ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListenerCache theEntry); + + @EventListener(ContextRefreshedEvent.class) + public void start(); + + @EventListener(ContextClosedEvent.class) + public void shutdown(); } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerCacheRefresherImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerCacheRefresherImpl.java index 5c8bbbe031e..38ceb2b24b8 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerCacheRefresherImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerCacheRefresherImpl.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import com.google.common.annotations.VisibleForTesting; +import jakarta.transaction.Transactional; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IIdType; import org.quartz.JobExecutionContext; @@ -57,7 +58,7 @@ public class ResourceChangeListenerCacheRefresherImpl /** * All cache entries are checked at this interval to see if they need to be refreshed */ - static long LOCAL_REFRESH_INTERVAL_MS = 10 * DateUtils.MILLIS_PER_SECOND; + static final long LOCAL_REFRESH_INTERVAL_MS = 10 * DateUtils.MILLIS_PER_SECOND; @Autowired private IResourceVersionSvc myResourceVersionSvc; @@ -133,8 +134,12 @@ public class ResourceChangeListenerCacheRefresherImpl } @Override + // Suspend any current transaction while we sync with the db. + // This avoids lock conflicts while reading the resource versions. + @Transactional(Transactional.TxType.NOT_SUPPORTED) public ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListenerCache theCache) { ResourceChangeResult retVal = new ResourceChangeResult(); + if (isStopping()) { ourLog.info("Context is stopping, aborting cache refresh"); return retVal; @@ -146,6 +151,7 @@ public class ResourceChangeListenerCacheRefresherImpl SearchParameterMap searchParamMap = theCache.getSearchParameterMap(); ResourceVersionMap newResourceVersionMap = myResourceVersionSvc.getVersionMap(theCache.getResourceName(), searchParamMap); + retVal = retVal.plus(notifyListener(theCache, newResourceVersionMap)); return retVal; @@ -154,7 +160,7 @@ public class ResourceChangeListenerCacheRefresherImpl /** * Notify a listener with all matching resources if it hasn't been initialized yet, otherwise only notify it if * any resources have changed - * @param theCache + * @param theCache the target * @param theNewResourceVersionMap the measured new resources * @return the list of created, updated and deleted ids */ diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java index 03813b04c3f..cf17cf5af41 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/partition/IRequestPartitionHelperSvc.java @@ -34,39 +34,119 @@ public interface IRequestPartitionHelperSvc { @Nonnull RequestPartitionId determineReadPartitionForRequest( - @Nullable RequestDetails theRequest, ReadPartitionIdRequestDetails theDetails); + @Nullable RequestDetails theRequest, @Nonnull ReadPartitionIdRequestDetails theDetails); + /** + * Determine partition to use when performing a server operation such as $bulk-import, $bulk-export, $reindex etc. + * @param theRequest the request details from the context of the call + * @param theOperationName the explicit name of the operation + * @return the partition id which should be used for the operation + */ + @Nonnull + default RequestPartitionId determineReadPartitionForRequestForServerOperation( + @Nullable RequestDetails theRequest, @Nonnull String theOperationName) { + ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forServerOperation(theOperationName); + return determineReadPartitionForRequest(theRequest, details); + } + + /** + * Determine partition to use when performing database reads based on a resource instance. + * @param theRequest the request details from the context of the call + * @param theId the id of the resource instance + * @return the partition id which should be used for the database read + */ @Nonnull default RequestPartitionId determineReadPartitionForRequestForRead( - RequestDetails theRequest, String theResourceType, @Nonnull IIdType theId) { + @Nullable RequestDetails theRequest, @Nonnull IIdType theId) { + ReadPartitionIdRequestDetails details = + ReadPartitionIdRequestDetails.forRead(theId.getResourceType(), theId, theId.hasVersionIdPart()); + return determineReadPartitionForRequest(theRequest, details); + } + + /** + * Determine partition to use when performing database reads against a certain resource type based on a resource instance. + * @param theRequest the request details from the context of the call + * @param theResourceType the resource type + * @param theId the id of the resource instance + * @return the partition id which should be used for the database read + */ + @Nonnull + default RequestPartitionId determineReadPartitionForRequestForRead( + @Nullable RequestDetails theRequest, @Nonnull String theResourceType, @Nonnull IIdType theId) { ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forRead(theResourceType, theId, theId.hasVersionIdPart()); return determineReadPartitionForRequest(theRequest, details); } + /** + * Determine partition to use when performing a database search against a certain resource type. + * @param theRequest the request details from the context of the call + * @param theResourceType the resource type + * @return the partition id which should be used for the database search + */ + @Nonnull + default RequestPartitionId determineReadPartitionForRequestForSearchType( + @Nullable RequestDetails theRequest, @Nonnull String theResourceType) { + ReadPartitionIdRequestDetails details = + ReadPartitionIdRequestDetails.forSearchType(theResourceType, SearchParameterMap.newSynchronous(), null); + return determineReadPartitionForRequest(theRequest, details); + } + + /** + * Determine partition to use when performing a database search based on a resource type and other search parameters. + * @param theRequest the request details from the context of the call + * @param theResourceType the resource type + * @param theParams the search parameters + * @return the partition id which should be used for the database search + */ + @Nonnull + default RequestPartitionId determineReadPartitionForRequestForSearchType( + @Nullable RequestDetails theRequest, + @Nonnull String theResourceType, + @Nonnull SearchParameterMap theParams) { + ReadPartitionIdRequestDetails details = + ReadPartitionIdRequestDetails.forSearchType(theResourceType, theParams, null); + return determineReadPartitionForRequest(theRequest, details); + } + + /** + * Determine partition to use when performing a database search based on a resource type, search parameters and a conditional target resource (if available). + * @param theRequest the request details from the context of the call + * @param theResourceType the resource type + * @param theParams the search parameters + * @param theConditionalOperationTargetOrNull the conditional target resource + * @return the partition id which should be used for the database search + */ @Nonnull default RequestPartitionId determineReadPartitionForRequestForSearchType( RequestDetails theRequest, String theResourceType, SearchParameterMap theParams, IBaseResource theConditionalOperationTargetOrNull) { + SearchParameterMap searchParameterMap = theParams != null ? theParams : SearchParameterMap.newSynchronous(); ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forSearchType( - theResourceType, theParams, theConditionalOperationTargetOrNull); + theResourceType, searchParameterMap, theConditionalOperationTargetOrNull); return determineReadPartitionForRequest(theRequest, details); } RequestPartitionId determineGenericPartitionForRequest(RequestDetails theRequestDetails); + /** + * Determine partition to use when performing the history operation based on a resource type and resource instance. + * @param theRequest the request details from the context of the call + * @param theResourceType the resource type + * @param theIdType the id of the resource instance + * @return the partition id which should be used for the history operation + */ @Nonnull default RequestPartitionId determineReadPartitionForRequestForHistory( - RequestDetails theRequest, String theResourceType, IIdType theIdType) { + @Nullable RequestDetails theRequest, String theResourceType, IIdType theIdType) { ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(theResourceType, theIdType); return determineReadPartitionForRequest(theRequest, details); } - @Nonnull default void validateHasPartitionPermissions( - RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) {} + @Nonnull RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) {} @Nonnull RequestPartitionId determineCreatePartitionForRequest( diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 6e6bd6e29fa..a62f7bbf10f 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java index 55ab2ad7d0d..8cf37a93587 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java @@ -136,7 +136,7 @@ public class SubscriptionMatcherInterceptor { // Even though the resource is being written, the subscription will be interacting with it by effectively // "reading" it so we set the RequestPartitionId as a read request RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead( - theRequest, theNewResource.getIdElement().getResourceType(), theNewResource.getIdElement()); + theRequest, theNewResource.getIdElement()); return new ResourceModifiedMessage( myFhirContext, theNewResource, theOperationType, theRequest, requestPartitionId); } diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index a13ca8df9d6..f59f6c8e55d 100644 --- a/hapi-fhir-jpaserver-test-dstu2/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/pom.xml b/hapi-fhir-jpaserver-test-dstu3/pom.xml index ccf4ea12fc8..aa8d03137b7 100644 --- a/hapi-fhir-jpaserver-test-dstu3/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu3/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index d5c500e4b17..c5ac179edbe 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -313,7 +313,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { .setSystem(codeSystem.getUrl()) .addFilter() .setProperty("concept") - .setOp(FilterOperator.ISA) + .setOp(FilterOperator.DESCENDENTOF) .setValue("dogs"); myValueSetDao.create(valueSet, mySrd); @@ -504,7 +504,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { logAndValidateValueSet(result); ArrayList codes = toCodesContains(result.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("childAAA", "childAAB")); + assertThat(codes, containsInAnyOrder("childAA", "childAAA", "childAAB")); } @@ -535,7 +535,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { logAndValidateValueSet(result); ArrayList codes = toCodesContains(result.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("childAAA", "childAAB")); + assertThat(codes, containsInAnyOrder("childAA", "childAAA", "childAAB")); } @@ -650,7 +650,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { ValueSet vs = new ValueSet(); ConceptSetComponent include = vs.getCompose().addInclude(); include.setSystem(URL_MY_CODE_SYSTEM); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); ValueSet result = myValueSetDao.expand(vs, null); logAndValidateValueSet(result); @@ -669,7 +669,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { vs = new ValueSet(); include = vs.getCompose().addInclude(); include.setSystem(URL_MY_CODE_SYSTEM); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); result = myValueSetDao.expand(vs, null); logAndValidateValueSet(result); diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java index aeb4cc7328c..98dd6fb4ec1 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java @@ -230,7 +230,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(theCodeSystemUrl); include.setVersion(theValueSetVersion); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); return myLocalVs; } diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/validator/AttachmentUtilTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/validator/AttachmentUtilTest.java index 072b1a5d6b2..fa622067321 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/validator/AttachmentUtilTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/validator/AttachmentUtilTest.java @@ -1,11 +1,16 @@ package ca.uhn.fhir.validator; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.primitive.CodeDt; import ca.uhn.fhir.util.AttachmentUtil; import org.hl7.fhir.instance.model.api.ICompositeType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.Attachment; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; public class AttachmentUtilTest { @@ -53,4 +58,18 @@ public class AttachmentUtilTest { String encoded = ctx.newJsonParser().encodeResourceToString(communication); assertEquals("{\"resourceType\":\"Communication\",\"payload\":[{\"contentAttachment\":{\"contentType\":\"text/plain\",\"data\":\"AAECAw==\",\"url\":\"http://foo\",\"size\":123}}]}", encoded); } + + @Test + public void testGetOrCreateContentTypeOnEmptyAttachmentR4(){ + FhirContext ctx = FhirContext.forR4Cached(); + Attachment attachment = (Attachment) AttachmentUtil.newInstance(ctx); + + assertNull(attachment.getContentType()); + + IPrimitiveType contentType = AttachmentUtil.getOrCreateContentType(ctx, attachment); + + contentType.setValueAsString("text/plain"); + + assertNotNull(attachment.getContentType()); + } } diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index a423489273a..f2e0117e89d 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java index 49aabba8e21..33cc8c258fc 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java @@ -32,6 +32,7 @@ import ca.uhn.fhir.util.JsonUtil; import ca.uhn.fhir.util.SearchParameterUtil; import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; +import jakarta.annotation.Nonnull; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; @@ -62,7 +63,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -71,7 +71,6 @@ import java.util.Set; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -1245,7 +1244,7 @@ public class BulkDataExportProviderTest { private class MyRequestPartitionHelperSvc extends RequestPartitionHelperSvc { @Override - public @NotNull RequestPartitionId determineReadPartitionForRequest(RequestDetails theRequest, ReadPartitionIdRequestDetails theDetails) { + public @NotNull RequestPartitionId determineReadPartitionForRequest(@Nonnull RequestDetails theRequest, @Nonnull ReadPartitionIdRequestDetails theDetails) { assert theRequest != null; if (myPartitionName.equals(theRequest.getTenantId())) { return myRequestPartitionId; @@ -1255,7 +1254,7 @@ public class BulkDataExportProviderTest { } @Override - public void validateHasPartitionPermissions(RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { + public void validateHasPartitionPermissions(@Nonnull RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { if (!myPartitionName.equals(theRequest.getTenantId()) && theRequest.getTenantId() != null) { throw new ForbiddenOperationException("User does not have access to resources on the requested partition"); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportUseCaseTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportUseCaseTest.java index 6899c125cb7..88e989e2ebd 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportUseCaseTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportUseCaseTest.java @@ -87,6 +87,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -299,9 +300,9 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { assertThat(result.getRequiresAccessToken(), is(equalTo(true))); assertThat(result.getTransactionTime(), is(notNullValue())); assertEquals(result.getOutput().size(), 3); - assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Patient")).collect(Collectors.toList()).size()); - assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Observation")).collect(Collectors.toList()).size()); - assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Encounter")).collect(Collectors.toList()).size()); + assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Patient")).count()); + assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Observation")).count()); + assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Encounter")).count()); //We assert specifically on content as the deserialized version will "helpfully" fill in missing fields. assertThat(responseContent, containsString("\"error\" : [ ]")); @@ -338,8 +339,8 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { assertThat(result.getRequiresAccessToken(), is(equalTo(true))); assertThat(result.getTransactionTime(), is(notNullValue())); assertEquals(result.getOutput().size(), 1); - assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Patient")).collect(Collectors.toList()).size()); - assertEquals(0, result.getOutput().stream().filter(o -> o.getType().equals("Binary")).collect(Collectors.toList()).size()); + assertEquals(1, result.getOutput().stream().filter(o -> o.getType().equals("Patient")).count()); + assertEquals(0, result.getOutput().stream().filter(o -> o.getType().equals("Binary")).count()); //We assert specifically on content as the deserialized version will "helpfully" fill in missing fields. assertThat(responseContent, containsString("\"error\" : [ ]")); @@ -381,7 +382,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { } HashSet types = Sets.newHashSet("Patient"); - BulkExportJobResults bulkExportJobResults = startSystemBulkExportJobAndAwaitCompletion(types, new HashSet()); + BulkExportJobResults bulkExportJobResults = startSystemBulkExportJobAndAwaitCompletion(types, new HashSet<>()); Map> resourceTypeToBinaryIds = bulkExportJobResults.getResourceTypeToBinaryIds(); assertThat(resourceTypeToBinaryIds.get("Patient"), hasSize(1)); String patientBinaryId = resourceTypeToBinaryIds.get("Patient").get(0); @@ -477,7 +478,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { String entities = myJobInstanceRepository .findAll() .stream() - .map(t -> t.toString()) + .map(Batch2JobInstanceEntity::toString) .collect(Collectors.joining("\n * ")); ourLog.info("Entities:\n * " + entities); }); @@ -492,6 +493,41 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { assertEquals(patientCount, jobInstance.getCombinedRecordsProcessed()); } + @Test + public void testEmptyExport() { + BulkExportJobParameters options = new BulkExportJobParameters(); + options.setResourceTypes(Collections.singleton("Patient")); + options.setFilters(Collections.emptySet()); + options.setExportStyle(BulkExportJobParameters.ExportStyle.SYSTEM); + options.setOutputFormat(Constants.CT_FHIR_NDJSON); + + JobInstanceStartRequest startRequest = new JobInstanceStartRequest(); + startRequest.setJobDefinitionId(Batch2JobDefinitionConstants.BULK_EXPORT); + startRequest.setParameters(options); + Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(mySrd, startRequest); + + assertNotNull(startResponse); + + final String jobId = startResponse.getInstanceId(); + + // Run a scheduled pass to build the export + myBatch2JobHelper.awaitJobCompletion(startResponse.getInstanceId()); + runInTransaction(() -> { + String entities = myJobInstanceRepository + .findAll() + .stream() + .map(Batch2JobInstanceEntity::toString) + .collect(Collectors.joining("\n * ")); + ourLog.info("Entities:\n * " + entities); + }); + + final Optional optJobInstance = myJobPersistence.fetchInstance(jobId); + assertNotNull(optJobInstance); + assertTrue(optJobInstance.isPresent()); + assertThat(optJobInstance.get().getReport(), + containsString("Export complete, but no data to generate report for job instance:")); + } + private void logContentTypeAndResponse(Header[] headers, String response) { ourLog.info("**************************"); ourLog.info("Content-Type is: {}", headers[0]); @@ -542,7 +578,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { // test HashSet types = Sets.newHashSet("Patient", "Observation"); - BulkExportJobResults bulkExportJobResults = startPatientBulkExportJobAndAwaitResults(types, new HashSet(), "ha"); + BulkExportJobResults bulkExportJobResults = startPatientBulkExportJobAndAwaitResults(types, new HashSet<>(), "ha"); Map> typeToResources = convertJobResultsToResources(bulkExportJobResults); assertThat(typeToResources.get("Patient"), hasSize(1)); assertThat(typeToResources.get("Observation"), hasSize(1)); @@ -605,6 +641,34 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { assertTrue(patientIds.contains(resourceId)); } } + + @Test + public void testExportEmptyResult() { + BulkExportJobParameters options = new BulkExportJobParameters(); + options.setResourceTypes(Sets.newHashSet("Patient")); + options.setExportStyle(BulkExportJobParameters.ExportStyle.PATIENT); + options.setOutputFormat(Constants.CT_FHIR_NDJSON); + + JobInstanceStartRequest startRequest = new JobInstanceStartRequest(); + startRequest.setJobDefinitionId(Batch2JobDefinitionConstants.BULK_EXPORT); + startRequest.setParameters(options); + Batch2JobStartResponse job = myJobCoordinator.startInstance(mySrd, startRequest); + myBatch2JobHelper.awaitJobCompletion(job.getInstanceId(), 60); + ourLog.debug("Job status after awaiting - {}", myJobCoordinator.getInstance(job.getInstanceId()).getStatus()); + await() + .atMost(300, TimeUnit.SECONDS) + .until(() -> { + StatusEnum status = myJobCoordinator.getInstance(job.getInstanceId()).getStatus(); + if (!StatusEnum.COMPLETED.equals(status)) { + fail("Job status was changed from COMPLETE to " + status); + } + return myJobCoordinator.getInstance(job.getInstanceId()).getReport() != null; + }); + + String report = myJobCoordinator.getInstance(job.getInstanceId()).getReport(); + assertThat(report, + containsString("Export complete, but no data to generate report for job instance:")); + } } @@ -1081,7 +1145,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { } } ] - } + } """; Bundle bundle = parser.parseResource(Bundle.class, bundleStr); myClient.transaction().withBundle(bundle).execute(); @@ -1218,6 +1282,21 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { assertThat(typeToContents.get("Patient"), not(containsString("POG2"))); } + @Test + public void testExportEmptyResult() { + Group group = new Group(); + group.setId("Group/G-empty"); + group.setActive(true); + myClient.update().resource(group).execute(); + + HashSet resourceTypes = Sets.newHashSet("Patient"); + BulkExportJobResults bulkExportJobResults = startGroupBulkExportJobAndAwaitCompletion( + resourceTypes, new HashSet<>(), "G-empty"); + + assertThat(bulkExportJobResults.getReportMsg(), + startsWith("Export complete, but no data to generate report for job instance:")); + } + @Test public void testGroupBulkExportMultipleResourceTypes() { Patient patient = new Patient(); @@ -1398,7 +1477,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { Map> convertJobResultsToResources(BulkExportJobResults theResults) { Map stringStringMap = convertJobResultsToStringContents(theResults); Map> typeToResources = new HashMap<>(); - stringStringMap.entrySet().forEach(entry -> typeToResources.put(entry.getKey(), convertNDJSONToResources(entry.getValue()))); + stringStringMap.forEach((key, value) -> typeToResources.put(key, convertNDJSONToResources(value))); return typeToResources; } @@ -1412,8 +1491,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { private String getBinaryContentsAsString(String theBinaryId) { Binary binary = myBinaryDao.read(new IdType(theBinaryId)); assertEquals(Constants.CT_FHIR_NDJSON, binary.getContentType()); - String contents = new String(binary.getContent(), Constants.CHARSET_UTF8); - return contents; + return new String(binary.getContent(), Constants.CHARSET_UTF8); } BulkExportJobResults startGroupBulkExportJobAndAwaitCompletion(HashSet theResourceTypes, HashSet theFilters, String theGroupId) { @@ -1509,8 +1587,7 @@ public class BulkExportUseCaseTest extends BaseResourceProviderR4Test { await().atMost(300, TimeUnit.SECONDS).until(() -> myJobCoordinator.getInstance(jobInstanceId).getReport() != null); String report = myJobCoordinator.getInstance(jobInstanceId).getReport(); - BulkExportJobResults results = JsonUtil.deserialize(report, BulkExportJobResults.class); - return results; + return JsonUtil.deserialize(report, BulkExportJobResults.class); } private void verifyBulkExportResults(String theGroupId, HashSet theFilters, List theContainedList, List theExcludedList) { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportWithPatientIdPartitioningTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportWithPatientIdPartitioningTest.java new file mode 100644 index 00000000000..c48de349bf1 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkExportWithPatientIdPartitioningTest.java @@ -0,0 +1,88 @@ +package ca.uhn.fhir.jpa.bulk; + +import ca.uhn.fhir.jpa.interceptor.PatientIdPartitionInterceptor; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.provider.BulkDataExportProvider; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import org.apache.http.Header; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class BulkExportWithPatientIdPartitioningTest extends BaseResourceProviderR4Test { + private final Logger ourLog = LoggerFactory.getLogger(BulkExportWithPatientIdPartitioningTest.class); + + @Autowired + private ISearchParamExtractor mySearchParamExtractor; + + private PatientIdPartitionInterceptor myPatientIdPartitionInterceptor; + + @BeforeEach + public void before() { + myPatientIdPartitionInterceptor = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); + myInterceptorRegistry.registerInterceptor(myPatientIdPartitionInterceptor); + myPartitionSettings.setPartitioningEnabled(true); + myPartitionSettings.setUnnamedPartitionMode(true); + } + + @AfterEach + public void after() { + myInterceptorRegistry.unregisterInterceptor(myPatientIdPartitionInterceptor); + myPartitionSettings.setPartitioningEnabled(new PartitionSettings().isPartitioningEnabled()); + myPartitionSettings.setUnnamedPartitionMode(new PartitionSettings().isUnnamedPartitionMode()); + } + + @Test + public void testSystemBulkExport_withResourceType_success() throws IOException { + HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + ProviderConstants.OPERATION_EXPORT); + post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC); + post.addHeader(BulkDataExportProvider.PARAM_EXPORT_TYPE, "Patient"); + post.addHeader(BulkDataExportProvider.PARAM_EXPORT_TYPE_FILTER, "Patient?"); + + try (CloseableHttpResponse postResponse = myServer.getHttpClient().execute(post)) { + ourLog.info("Response: {}", postResponse); + assertEquals(202, postResponse.getStatusLine().getStatusCode()); + assertEquals("Accepted", postResponse.getStatusLine().getReasonPhrase()); + } + } + + @Test + public void testSystemBulkExport_withResourceType_pollSuccessful() throws IOException { + HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + ProviderConstants.OPERATION_EXPORT); + post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC); + post.addHeader(BulkDataExportProvider.PARAM_EXPORT_TYPE, "Patient"); // ignored when computing partition + post.addHeader(BulkDataExportProvider.PARAM_EXPORT_TYPE_FILTER, "Patient?"); + + String locationUrl; + + try (CloseableHttpResponse postResponse = myServer.getHttpClient().execute(post)) { + ourLog.info("Response: {}", postResponse); + assertEquals(202, postResponse.getStatusLine().getStatusCode()); + assertEquals("Accepted", postResponse.getStatusLine().getReasonPhrase()); + + Header locationHeader = postResponse.getFirstHeader(Constants.HEADER_CONTENT_LOCATION); + assertNotNull(locationHeader); + locationUrl = locationHeader.getValue(); + } + + HttpGet get = new HttpGet(locationUrl); + try (CloseableHttpResponse postResponse = myServer.getHttpClient().execute(get)) { + ourLog.info("Response: {}", postResponse); + assertEquals(202, postResponse.getStatusLine().getStatusCode()); + } + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportR4Test.java index 62cf902d570..d8d30c4a61e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportR4Test.java @@ -33,11 +33,13 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Pageable; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.blankOrNullString; import static org.hamcrest.Matchers.containsString; @@ -52,7 +54,9 @@ import static org.junit.jupiter.api.Assertions.fail; public class BulkImportR4Test extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(BulkImportR4Test.class); - private final BulkImportFileServlet myBulkImportFileServlet = new BulkImportFileServlet(); + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private final BulkImportFileServlet myBulkImportFileServlet = new BulkImportFileServlet(USERNAME, PASSWORD); @RegisterExtension private final HttpServletExtension myHttpServletExtension = new HttpServletExtension() .withServlet(myBulkImportFileServlet); @@ -76,6 +80,45 @@ public class BulkImportR4Test extends BaseJpaR4Test { await().until(() -> channel.getQueueSizeForUnitTest() == 0); } + + @Test + public void testBulkImportFailsWith403OnBadCredentials() { + + BulkImportJobParameters parameters = new BulkImportJobParameters(); + String url = myHttpServletExtension.getBaseUrl() + "/download?index=test"; // Name doesnt matter, its going to fail with 403 anyhow + parameters.addNdJsonUrl(url); + JobInstanceStartRequest request = new JobInstanceStartRequest(); + request.setJobDefinitionId(BulkImportAppCtx.JOB_BULK_IMPORT_PULL); + request.setParameters(parameters); + + // Execute + Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request); + String instanceId = startResponse.getInstanceId(); + assertThat(instanceId, not(blankOrNullString())); + ourLog.info("Execution got ID: {}", instanceId); + + // Verify + await().atMost(120, TimeUnit.SECONDS).until(() -> { + myJobCleanerService.runMaintenancePass(); + JobInstance instance = myJobCoordinator.getInstance(instanceId); + return instance.getStatus(); + }, equalTo(StatusEnum.FAILED)); + + //No resources stored + runInTransaction(() -> { + assertEquals(0, myResourceTableDao.count()); + }); + + + //Should have 403 + runInTransaction(() -> { + JobInstance instance = myJobCoordinator.getInstance(instanceId); + ourLog.info("Instance details:\n{}", JsonUtil.serialize(instance, true)); + assertEquals(1, instance.getErrorCount()); + assertThat(instance.getErrorMessage(), is(containsString("Received HTTP 403"))); + }); + + } @Test public void testRunBulkImport() { // Setup @@ -84,6 +127,8 @@ public class BulkImportR4Test extends BaseJpaR4Test { List indexes = addFiles(fileCount); BulkImportJobParameters parameters = new BulkImportJobParameters(); + + parameters.setHttpBasicCredentials(USERNAME + ":" + PASSWORD); for (String next : indexes) { String url = myHttpServletExtension.getBaseUrl() + "/download?index=" + next; parameters.addNdJsonUrl(url); @@ -132,6 +177,7 @@ public class BulkImportR4Test extends BaseJpaR4Test { List indexes = addFiles(fileCount); BulkImportJobParameters parameters = new BulkImportJobParameters(); + parameters.setHttpBasicCredentials(USERNAME + ":" + PASSWORD); for (String next : indexes) { String url = myHttpServletExtension.getBaseUrl() + "/download?index=" + next; parameters.addNdJsonUrl(url); @@ -219,6 +265,7 @@ public class BulkImportR4Test extends BaseJpaR4Test { indexes.add(myBulkImportFileServlet.registerFileByContents("{\"resourceType\":\"Foo\"}")); BulkImportJobParameters parameters = new BulkImportJobParameters(); + parameters.setHttpBasicCredentials(USERNAME + ":" + PASSWORD); for (String next : indexes) { String url = myHttpServletExtension.getBaseUrl() + "/download?index=" + next; parameters.addNdJsonUrl(url); @@ -260,6 +307,7 @@ public class BulkImportR4Test extends BaseJpaR4Test { BulkImportJobParameters parameters = new BulkImportJobParameters(); String url = myHttpServletExtension.getBaseUrl() + "/download?index=FOO"; parameters.addNdJsonUrl(url); + parameters.setHttpBasicCredentials(USERNAME + ":" + PASSWORD); JobInstanceStartRequest request = new JobInstanceStartRequest(); request.setJobDefinitionId(BulkImportAppCtx.JOB_BULK_IMPORT_PULL); @@ -328,7 +376,9 @@ public class BulkImportR4Test extends BaseJpaR4Test { JobInstanceStartRequest request = new JobInstanceStartRequest(); request.setJobDefinitionId(BulkImportAppCtx.JOB_BULK_IMPORT_PULL); - request.setParameters(new BulkImportJobParameters()); + BulkImportJobParameters parameters = new BulkImportJobParameters(); + parameters.setHttpBasicCredentials(USERNAME + ":" + PASSWORD); + request.setParameters(parameters); // Execute diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportWithPatientIdPartitioningTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportWithPatientIdPartitioningTest.java new file mode 100644 index 00000000000..7f5c3731607 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt2/BulkImportWithPatientIdPartitioningTest.java @@ -0,0 +1,112 @@ +package ca.uhn.fhir.jpa.bulk.imprt2; + +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.api.IJobMaintenanceService; +import ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx; +import ca.uhn.fhir.batch2.jobs.imprt.BulkImportFileServlet; +import ca.uhn.fhir.batch2.jobs.imprt.BulkImportJobParameters; +import ca.uhn.fhir.batch2.model.JobInstance; +import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.batch2.model.StatusEnum; +import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; +import ca.uhn.fhir.jpa.interceptor.PatientIdPartitionInterceptor; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.fhir.test.utilities.server.HttpServletExtension; +import ca.uhn.fhir.util.JsonUtil; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.blankOrNullString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This test class has just one test. It can potentially be moved + */ +public class BulkImportWithPatientIdPartitioningTest extends BaseJpaR4Test { + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final BulkImportFileServlet ourBulkImportFileServlet = new BulkImportFileServlet(USERNAME, PASSWORD); + + @RegisterExtension + public static HttpServletExtension myHttpServletExtension = new HttpServletExtension().withServlet(ourBulkImportFileServlet); + + @Autowired + private IJobCoordinator myJobCoordinator; + @Autowired + private IJobMaintenanceService myJobCleanerService; + + @Autowired + private ISearchParamExtractor mySearchParamExtractor; + + private PatientIdPartitionInterceptor myPatientIdPartitionInterceptor; + + @BeforeEach + public void before() { + myPatientIdPartitionInterceptor = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); + myInterceptorRegistry.registerInterceptor(myPatientIdPartitionInterceptor); + myPartitionSettings.setPartitioningEnabled(true); + myPartitionSettings.setUnnamedPartitionMode(true); + } + + @AfterEach + public void after() { + myInterceptorRegistry.unregisterInterceptor(myPatientIdPartitionInterceptor); + myPartitionSettings.setPartitioningEnabled(new PartitionSettings().isPartitioningEnabled()); + myPartitionSettings.setUnnamedPartitionMode(new PartitionSettings().isUnnamedPartitionMode()); + } + + @Test + public void testBulkImport_withOneResource_successful() { + // Setup + + Patient p = new Patient().setActive(true); + p.setId("P1"); + String fileContents = getFhirContext().newJsonParser().encodeResourceToString(p); + String id = ourBulkImportFileServlet.registerFileByContents(fileContents); + + BulkImportJobParameters parameters = new BulkImportJobParameters(); + parameters.setHttpBasicCredentials(USERNAME + ":" + PASSWORD); + parameters.addNdJsonUrl(myHttpServletExtension.getBaseUrl() + "/download?index=" + id); + + JobInstanceStartRequest request = new JobInstanceStartRequest(); + request.setJobDefinitionId(BulkImportAppCtx.JOB_BULK_IMPORT_PULL); + request.setParameters(parameters); + + // Execute + + Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(request); + String instanceId = startResponse.getInstanceId(); + assertThat(instanceId, not(blankOrNullString())); + ourLog.info("Execution got ID: {}", instanceId); + + // Verify + + await().atMost(120, TimeUnit.SECONDS).until(() -> { + myJobCleanerService.runMaintenancePass(); + JobInstance instance = myJobCoordinator.getInstance(instanceId); + return instance.getStatus(); + }, equalTo(StatusEnum.COMPLETED)); + + runInTransaction(() -> { + assertEquals(1, myResourceTableDao.count()); + }); + + runInTransaction(() -> { + JobInstance instance = myJobCoordinator.getInstance(instanceId); + ourLog.info("Instance details:\n{}", JsonUtil.serialize(instance, true)); + assertEquals(0, instance.getErrorCount()); + }); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerRegistryImplIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerRegistryImplIT.java index 842e00c3a9a..6c4ff5e7730 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerRegistryImplIT.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerRegistryImplIT.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.test.concurrency.IPointcutLatch; import ca.uhn.test.concurrency.PointcutLatch; import org.apache.commons.lang3.time.DateUtils; @@ -36,7 +37,7 @@ public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test { @Autowired ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry; @Autowired - ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher; + IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher; private final static String RESOURCE_NAME = "Patient"; private TestCallback myMaleTestCallback = new TestCallback("MALE"); @@ -130,9 +131,10 @@ public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test { return patient; } - private IdDt createPatientAndRefreshCache(Patient thePatient, TestCallback theTestCallback, long theExpectedCount) throws InterruptedException { + private IdDt createPatientAndRefreshCache(Patient thePatient, TestCallback theTestCallback, long theExpectedCount) { IIdType retval = myPatientDao.create(thePatient).getId(); - ResourceChangeResult result = myResourceChangeListenerCacheRefresher.forceRefreshAllCachesForUnitTest(); + ResourceChangeResult result = ProxyUtil.getSingletonTarget(myResourceChangeListenerCacheRefresher, ResourceChangeListenerCacheRefresherImpl.class) + .forceRefreshAllCachesForUnitTest(); assertResult(result, theExpectedCount, 0, 0); return new IdDt(retval); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java index 6aff8e400a7..c68d4a77b1d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java @@ -6,55 +6,76 @@ import ca.uhn.fhir.batch2.jobs.parameters.UrlPartitioner; import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 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.api.svc.IIdHelperService; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.jpa.delete.DeleteConflictService; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; +import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.ResourceSearch; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.svc.MockHapiTransactionService; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 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.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import com.google.common.collect.Lists; +import jakarta.persistence.EntityManager; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationContext; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; -import jakarta.persistence.EntityManager; +import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.isNotNull; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -76,6 +97,9 @@ class BaseHapiFhirResourceDaoTest { @Mock private IJobCoordinator myJobCoordinator; + @Mock + private IJpaStorageResourceParser myJpaStorageResourceParser; + @Mock private UrlPartitioner myUrlPartitioner; @@ -91,6 +115,23 @@ class BaseHapiFhirResourceDaoTest { @Mock private ISearchBuilder myISearchBuilder; + @Mock + private MatchUrlService myMatchUrlService; + @Mock + private MatchResourceUrlService myMatchResourceUrlService; + + @Mock + private HapiTransactionService myTransactionService; + + @Mock + private DeleteConflictService myDeleteConflictService; + + @Mock + private IFhirSystemDao mySystemDao; + + @Mock + private ResourceSearchUrlSvc myResourceSearchUrlSvc; + @Captor private ArgumentCaptor mySearchParameterMapCaptor; @@ -100,6 +141,8 @@ class BaseHapiFhirResourceDaoTest { @InjectMocks private TestResourceDao mySvc; + private TestResourceDao mySpiedSvc; + @BeforeEach public void init() { // set our context @@ -108,6 +151,7 @@ class BaseHapiFhirResourceDaoTest { // the individual tests will have to start // by calling setup themselves mySvc.setContext(myFhirContext); + mySpiedSvc = spy(mySvc); } /** @@ -198,7 +242,7 @@ class BaseHapiFhirResourceDaoTest { )).thenReturn(jpaPid); when(myEntityManager.find( any(Class.class), - Mockito.anyLong() + anyLong() )).thenReturn(entity); // we don't stub myConfig.getResourceClientIdStrategy() // because even a null return isn't ANY... @@ -288,6 +332,97 @@ class BaseHapiFhirResourceDaoTest { ); } + @Nested + class DeleteThresholds { + private static final String URL = "Patient?_lastUpdated=gt2024-01-01"; + private static final RequestDetails REQUEST = new SystemRequestDetails(); + private static final DeleteMethodOutcome EXPECTED_DELETE_OUTCOME = new DeleteMethodOutcome(); + + @BeforeEach + void beforeEach() { + when(myStorageSettings.isDeleteEnabled()).thenReturn(true); + when(myMatchUrlService.getResourceSearch(URL)) + .thenReturn(new ResourceSearch(mock(RuntimeResourceDefinition.class), SearchParameterMap.newSynchronous(), RequestPartitionId.allPartitions())); + + // mocks for transaction handling: + final IHapiTransactionService.IExecutionBuilder mockExecutionBuilder = mock(IHapiTransactionService.IExecutionBuilder.class); + when(mockExecutionBuilder.withTransactionDetails(any(TransactionDetails.class))).thenReturn(mockExecutionBuilder); + when(myTransactionService.withRequest(REQUEST)).thenReturn(mockExecutionBuilder); + final Answer answer = theInvocationOnMock -> { + final TransactionCallback arg = theInvocationOnMock.getArgument(0); + return arg.doInTransaction(mock(TransactionStatus.class)); + }; + when(mockExecutionBuilder.execute(ArgumentMatchers.>any())) + .thenAnswer(answer); + } + + @ParameterizedTest + @MethodSource("thresholdsAndResourceIds_Pass") + void deleteByUrlConsiderThresholdUnder_Pass(long theThreshold, Set theResourceIds) { + if (theResourceIds.size() > 1) { + when(myStorageSettings.isAllowMultipleDelete()).thenReturn(true); + when(myStorageSettings.getRestDeleteByUrlResourceIdThreshold()).thenReturn(theThreshold); + } + + doReturn(EXPECTED_DELETE_OUTCOME).when(mySpiedSvc).deletePidList(any(), any(), any(), any(), any()); + + handleExpectedResourceIds(theResourceIds); + + final DeleteMethodOutcome deleteMethodOutcome = mySpiedSvc.deleteByUrl(URL, REQUEST); + assertEquals(EXPECTED_DELETE_OUTCOME, deleteMethodOutcome); + } + + @ParameterizedTest + @MethodSource("thresholdsAndResourceIds_Fail") + void deleteByUrlConsiderThreshold_Over_Fail(long theThreshold, Set theResourceIds) { + when(myStorageSettings.isAllowMultipleDelete()).thenReturn(true); + when(myStorageSettings.getRestDeleteByUrlResourceIdThreshold()).thenReturn(theThreshold); + + final Set expectedResourceIds = handleExpectedResourceIds(theResourceIds); + + try { + mySpiedSvc.deleteByUrl(URL, REQUEST); + fail(); + } catch (PreconditionFailedException exception) { + assertEquals(String.format("HAPI-2496: Failed to DELETE resources with match URL \"Patient?_lastUpdated=gt2024-01-01\" because the resolved number of resources: %s exceeds the threshold of %s", expectedResourceIds.size(), theThreshold), exception.getMessage()); + } + } + + private Set handleExpectedResourceIds(Set theResourceIds) { + final Set expectedResourceIds = theResourceIds.stream().map(JpaPid::fromId).collect(Collectors.toUnmodifiableSet()); + when(myMatchResourceUrlService.search(any(), any(), any(), any())).thenReturn(expectedResourceIds); + return expectedResourceIds; + } + + static Stream thresholdsAndResourceIds_Pass() { + return Stream.of( + Arguments.of(0, Collections.emptySet()), + Arguments.of(1, Collections.emptySet()), + Arguments.of(2, Collections.emptySet()), + Arguments.of(3, Collections.emptySet()), + Arguments.of(4, Collections.emptySet()), + Arguments.of(5, Collections.emptySet()), + Arguments.of(1, Set.of(1L)), + Arguments.of(2, Set.of(1L)), + Arguments.of(3, Set.of(1L)), + Arguments.of(4, Set.of(1L)), + Arguments.of(5, Set.of(1L)), + Arguments.of(4, Set.of(1L,2L,3L)), + Arguments.of(5, Set.of(1L,2L,3L)) + ); + } + + static Stream thresholdsAndResourceIds_Fail() { + return Stream.of( + Arguments.of(0, Set.of(1L,2L)), + Arguments.of(1, Set.of(1L,2L)), + Arguments.of(0, Set.of(1L,2L,3L)), + Arguments.of(1, Set.of(1L,2L,3L)), + Arguments.of(2, Set.of(1L,2L,3L)) + ); + } + } + static class TestResourceDao extends BaseHapiFhirResourceDao { @Override diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java index 31fecd59bdd..7c75a50ae87 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java @@ -11,14 +11,18 @@ import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.AuditEvent; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.DomainResource; import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Location; import org.hl7.fhir.r4.model.MessageHeader; @@ -27,22 +31,26 @@ import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Reference; +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.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; +import java.sql.Date; import java.util.ArrayList; import java.util.List; import static org.apache.commons.lang3.StringUtils.countMatches; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.in; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -1573,6 +1581,76 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1); } + @ParameterizedTest + @CsvSource({ + // search url expected count + "/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=1980-01-01, 1", // correct identifier, correct birthdate + "/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-1, 1", // correct birthdate, correct identifier + "/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=2000-01-01, 0", // correct identifier, incorrect birthdate + "/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-1, 0", // incorrect birthdate, correct identifier + "/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=1980-01-01, 0", // incorrect identifier, correct birthdate + "/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-2, 0", // correct birthdate, incorrect identifier + "/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=2000-01-01, 0", // incorrect identifier, incorrect birthdate + "/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-2, 0", // incorrect birthdate, incorrect identifier + }) + public void testMultipleChainedBundleCompositionSearchParameters(String theSearchUrl, int theExpectedCount) { + createSearchParameter("bundle-composition-patient-birthdate", + "composition.patient.birthdate", + "Bundle", + "Bundle.entry.resource.ofType(Patient).birthDate", + Enumerations.SearchParamType.DATE + ); + + createSearchParameter("bundle-composition-patient-identifier", + "composition.patient.identifier", + "Bundle", + "Bundle.entry.resource.ofType(Patient).identifier", + Enumerations.SearchParamType.TOKEN + ); + + createDocumentBundleWithPatientDetails("1980-01-01", "system", "value-1"); + + SearchParameterMap params = myMatchUrlService.getResourceSearch(theSearchUrl).getSearchParameterMap().setLoadSynchronous(true); + assertSearchReturns(myBundleDao, params, theExpectedCount); + } + + private void createSearchParameter(String theId, String theCode, String theBase, String theExpression, Enumerations.SearchParamType theType) { + SearchParameter searchParameter = new SearchParameter(); + searchParameter.setId(theId); + searchParameter.setCode(theCode); + searchParameter.setName(theCode); + searchParameter.setUrl("http://example.org/SearchParameter/" + theId); + searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); + searchParameter.addBase(theBase); + searchParameter.setType(theType); + searchParameter.setExpression(theExpression); + searchParameter = (SearchParameter) mySearchParameterDao.update(searchParameter, mySrd).getResource(); + mySearchParamRegistry.forceRefresh(); + assertNotNull(mySearchParamRegistry.getActiveSearchParam(theBase, searchParameter.getName())); + } + + private void createDocumentBundleWithPatientDetails(String theBirthDate, String theIdentifierSystem, String theIdentifierValue) { + Patient patient = new Patient(); + patient.setBirthDate(Date.valueOf(theBirthDate)); + patient.addIdentifier().setSystem(theIdentifierSystem).setValue(theIdentifierValue); + patient = (Patient) myPatientDao.create(patient, mySrd).getResource(); + assertSearchReturns(myPatientDao, SearchParameterMap.newSynchronous(), 1); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + Composition composition = new Composition(); + composition.setType(new CodeableConcept().addCoding(new Coding().setCode("code").setSystem("http://example.org"))); + bundle.addEntry().setResource(composition); + composition.getSubject().setReference(patient.getIdElement().getValue()); + bundle.addEntry().setResource(patient); + myBundleDao.create(bundle, mySrd); + assertSearchReturns(myBundleDao, SearchParameterMap.newSynchronous(), 1); + } + + private void assertSearchReturns(IFhirResourceDao theDao, SearchParameterMap theSearchParams, int theExpectedCount){ + assertEquals(theExpectedCount, theDao.search(theSearchParams, mySrd).size()); + } + private void countUnionStatementsInGeneratedQuery(String theUrl, int theExpectedNumberOfUnions) throws IOException { myCaptureQueriesListener.clear(); searchAndReturnUnqualifiedVersionlessIdValues(theUrl); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index db30329cbad..a4747add0e2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; @@ -29,6 +30,7 @@ import ca.uhn.fhir.util.ClasspathUtil; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; import org.exparity.hamcrest.date.DateMatchers; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; @@ -39,6 +41,7 @@ import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.InstantType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Organization; @@ -48,8 +51,12 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.SampledData; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.PageRequest; @@ -60,6 +67,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -77,6 +85,7 @@ import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -1224,4 +1233,116 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { assertEquals(1, ids.size()); } + @Nested + class ConditionalCreates { + private static final String SYSTEM = "http://tempuri.org"; + private static final String VALUE_1 = "1"; + private static final String VALUE_2 = "2"; + + private final Task myTask1 = new Task() + .setStatus(Task.TaskStatus.DRAFT) + .setIntent(Task.TaskIntent.UNKNOWN) + .addIdentifier(new Identifier() + .setSystem(SYSTEM) + .setValue(VALUE_1)); + + private final Task myTask2 = new Task() + .setStatus(Task.TaskStatus.DRAFT) + .setIntent(Task.TaskIntent.UNKNOWN) + .addIdentifier(new Identifier() + .setSystem(SYSTEM) + .setValue(VALUE_2)) + .addBasedOn(new Reference().setReference("urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea")); + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testConditionalCreateDependsOnPOSTedResource(boolean theHasQuestionMark) { + final IFhirResourceDao taskDao = getTaskDao(); + taskDao.create(myTask1, new SystemRequestDetails()); + + final List allTasksPreBundle = searchAllTasks(); + assertEquals(1, allTasksPreBundle.size()); + final Task taskPreBundle = allTasksPreBundle.get(0); + assertEquals(VALUE_1, taskPreBundle.getIdentifier().get(0).getValue()); + assertEquals(SYSTEM, taskPreBundle.getIdentifier().get(0).getSystem()); + + final BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + + final String entryConditionalTemplate = "%sidentifier=http://tempuri.org|1"; + final String matchUrl = String.format(entryConditionalTemplate, theHasQuestionMark ? "?" : ""); + + bundleBuilder.addTransactionCreateEntry(myTask2) + .conditional(matchUrl); + + final List responseEntries = sendBundleAndGetResponse(bundleBuilder.getBundle()); + + assertEquals(1, responseEntries.size()); + + final Bundle.BundleEntryComponent bundleEntry = responseEntries.get(0); + + assertEquals("200 OK", bundleEntry.getResponse().getStatus()); + + final List allTasksPostBundle = searchAllTasks(); + assertEquals(1, allTasksPostBundle.size()); + final Task taskPostBundle = allTasksPostBundle.get(0); + assertEquals(VALUE_1, taskPostBundle.getIdentifier().get(0).getValue()); + assertEquals(SYSTEM, taskPostBundle.getIdentifier().get(0).getSystem()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testConditionalCreateDependsOnFirstEntryExisting(boolean theHasQuestionMark) { + final BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + + bundleBuilder.addTransactionCreateEntry(myTask1, "urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea") + .conditional("identifier=http://tempuri.org|1"); + + final String secondEntryConditionalTemplate = "%sidentifier=http://tempuri.org|2&based-on=urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"; + final String secondMatchUrl = String.format(secondEntryConditionalTemplate, theHasQuestionMark ? "?" : ""); + + bundleBuilder.addTransactionCreateEntry(myTask2) + .conditional(secondMatchUrl); + + final IBaseBundle requestBundle = bundleBuilder.getBundle(); + assertTrue(requestBundle instanceof Bundle); + + final List responseEntries = sendBundleAndGetResponse(requestBundle); + + assertEquals(2, responseEntries.size()); + assertEquals(Set.of("201 Created"), responseEntries.stream().map(Bundle.BundleEntryComponent::getResponse).map(Bundle.BundleEntryResponseComponent::getStatus).collect(Collectors.toUnmodifiableSet())); + + final List allTasksPostBundle = searchAllTasks(); + assertEquals(2, allTasksPostBundle.size()); + final Task taskPostBundle1 = allTasksPostBundle.get(0); + assertEquals(VALUE_1, taskPostBundle1.getIdentifier().get(0).getValue()); + assertEquals(SYSTEM, taskPostBundle1.getIdentifier().get(0).getSystem()); + final Task taskPostBundle2 = allTasksPostBundle.get(1); + assertEquals(VALUE_2, taskPostBundle2.getIdentifier().get(0).getValue()); + assertEquals(SYSTEM, taskPostBundle2.getIdentifier().get(0).getSystem()); + + final List task2BasedOn = taskPostBundle2.getBasedOn(); + assertEquals(1, task2BasedOn.size()); + final Reference task2BasedOnReference = task2BasedOn.get(0); + assertEquals(taskPostBundle1.getIdElement().toUnqualifiedVersionless().asStringValue(), task2BasedOnReference.getReference()); + } + } + + private List sendBundleAndGetResponse(IBaseBundle theRequestBundle) { + assertTrue(theRequestBundle instanceof Bundle); + + return mySystemDao.transaction(new SystemRequestDetails(), (Bundle)theRequestBundle).getEntry(); + } + + private List searchAllTasks() { + return unsafeCast(getTaskDao().search(SearchParameterMap.newSynchronous(), new SystemRequestDetails()).getAllResources()); + } + + private IFhirResourceDao getTaskDao() { + return unsafeCast(myDaoRegistry.getResourceDao("Task")); + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(Object theObject) { + return (T)theObject; + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index afbd4fff725..9e24d3e728a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -600,7 +600,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test fail(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome())); } myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(12, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + assertEquals(14, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); @@ -610,14 +610,14 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test myCaptureQueriesListener.clear(); myObservationDao.validate(obs, null, null, null, null, null, null); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); + assertEquals(6, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); myCaptureQueriesListener.logInsertQueriesForCurrentThread(); assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); myCaptureQueriesListener.logDeleteQueriesForCurrentThread(); assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size()); - assertEquals(0, myCaptureQueriesListener.getCommitCount()); + assertEquals(6, myCaptureQueriesListener.getCommitCount()); } /** @@ -3379,7 +3379,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); myCaptureQueriesListener.logInsertQueries(); assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(7, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(6, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); @@ -3462,7 +3462,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertEquals(8, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); - assertEquals(6, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(5, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 2d668f24e5d..ee36b6b6f19 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -23,7 +23,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; @@ -36,6 +35,7 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.param.CompositeParam; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.DateRangeParam; @@ -60,6 +60,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.HapiExtensions; import com.google.common.collect.Lists; +import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; @@ -151,7 +152,6 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; -import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; @@ -658,7 +658,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { myEncounterDao.search(map); fail(); } catch (InvalidRequestException e) { - assertEquals("Resource type \"Organization\" is not a valid target type for reference search parameter: Encounter:subject", e.getMessage()); + assertEquals(Msg.code(2495) + "Resource type \"Organization\" is not a valid target type for reference search parameter: Encounter:subject", e.getMessage()); } map = new SearchParameterMap(); @@ -3549,7 +3549,41 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { map.add(Task.SP_REQUESTER, new ReferenceParam(oid1.getValue())); ids = toUnqualifiedVersionlessIds(myTaskDao.search(map)); assertThat(ids, contains(tid1)); // NOT tid2 + } + @Test + public void testSearchWithValidTypedResourceReference_returnsCorrectly() { + // setup + IIdType encounterId = createEncounter(withIdentifier("http://example", "someValue")); + + MedicationAdministration ma = new MedicationAdministration() + .setContext(new Reference(encounterId)) + .setEffective(new DateTimeType()); + IIdType medicationAdministrationId = myMedicationAdministrationDao.create(ma, mySrd).getId(); + + // execute + ReferenceParam referenceParam = new ReferenceParam(encounterId.getResourceType(), null, encounterId.getIdPart()); + SearchParameterMap map = new SearchParameterMap().add(MedicationAdministration.SP_CONTEXT, referenceParam); + + // verify + List ids = toUnqualifiedVersionlessIds(myMedicationAdministrationDao.search(map, mySrd)); + assertEquals(1, ids.size()); + assertThat(ids, contains(medicationAdministrationId.toUnqualifiedVersionless())); + } + + @Test + public void testSearchWithInvalidTypedResourceReference_throwsUnsupportedResourceType() { + // execute + try { + ReferenceParam referenceParam = new ReferenceParam("abc", null, "123"); + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(MedicationAdministration.SP_CONTEXT, referenceParam); + + // verify + myMedicationAdministrationDao.search(map, mySrd); + fail(); + } catch (InvalidRequestException e) { + assertEquals(Msg.code(1250) + "Invalid/unsupported resource type: \"abc\"", e.getMessage()); + } } @Test diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java index 048e242d700..789e11b60f1 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java @@ -353,7 +353,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { .setSystem(codeSystem.getUrl()) .addFilter() .setProperty("concept") - .setOp(FilterOperator.ISA) + .setOp(FilterOperator.DESCENDENTOF) .setValue("dogs"); myValueSetDao.create(valueSet, mySrd); @@ -584,7 +584,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { logAndValidateValueSet(result); ArrayList codes = toCodesContains(result.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("childAAA", "childAAB")); + assertThat(codes, containsInAnyOrder("childAA", "childAAA", "childAAB")); } @@ -610,6 +610,34 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { logAndValidateValueSet(result); ArrayList codes = toCodesContains(result.getExpansion().getContains()); + assertEquals(3, codes.size()); + assertThat(codes, containsInAnyOrder("childAA", "childAAA", "childAAB")); + + } + + @Test + public void testExpandWithDescendentOfInExternalValueSetReindex() { + TermReindexingSvcImpl.setForceSaveDeferredAlwaysForUnitTest(true); + + createExternalCsAndLocalVs(); + + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); + myTerminologyDeferredStorageSvc.saveDeferred(); + myTerminologyDeferredStorageSvc.saveDeferred(); + myTerminologyDeferredStorageSvc.saveDeferred(); + + ValueSet vs = new ValueSet(); + ConceptSetComponent include = vs.getCompose().addInclude(); + include.setSystem(TermTestUtil.URL_MY_CODE_SYSTEM); + include.addFilter().setOp(FilterOperator.DESCENDENTOF).setValue("childAA").setProperty("concept"); + + ValueSet result = myValueSetDao.expand(vs, null); // breakpoint + logAndValidateValueSet(result); + + ArrayList codes = toCodesContains(result.getExpansion().getContains()); + assertEquals(2, codes.size()); assertThat(codes, containsInAnyOrder("childAAA", "childAAB")); } @@ -795,7 +823,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { ValueSet vs = new ValueSet(); ConceptSetComponent include = vs.getCompose().addInclude(); include.setSystem(TermTestUtil.URL_MY_CODE_SYSTEM); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); ValueSet result = myValueSetDao.expand(vs, null); logAndValidateValueSet(result); @@ -814,7 +842,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { vs = new ValueSet(); include = vs.getCompose().addInclude(); include.setSystem(TermTestUtil.URL_MY_CODE_SYSTEM); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); result = myValueSetDao.expand(vs, null); logAndValidateValueSet(result); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 29c85e9b7dc..61ef483c171 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -31,8 +31,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.validation.IValidatorModule; -import ca.uhn.fhir.validation.ResultSeverityEnum; -import ca.uhn.fhir.validation.ValidationResult; import org.apache.commons.io.IOUtils; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; @@ -49,10 +47,10 @@ import org.hl7.fhir.utilities.i18n.I18nConstants; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.AopTestUtils; @@ -1985,23 +1983,76 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } } - @Test - public void testValidateUsingDifferentialProfile() throws IOException { - StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); - myStructureDefinitionDao.create(sd); + @Nested + class TestValidateUsingDifferentialProfile { + private static final String PROFILE_URL = "http://example.com/fhir/StructureDefinition/patient-1a-extensions"; - Patient p = new Patient(); - p.getText().setStatus(Narrative.NarrativeStatus.GENERATED); - p.getText().getDiv().setValue("
hello
"); - p.getMeta().addProfile("http://example.com/fhir/StructureDefinition/patient-1a-extensions"); - p.setActive(true); + private static final Patient PATIENT_WITH_REAL_URL = createPatient(PROFILE_URL); + private static final Patient PATIENT_WITH_FAKE_URL = createPatient("https://www.i.do.not.exist.com"); - String raw = myFhirContext.newJsonParser().encodeResourceToString(p); - MethodOutcome outcome = myPatientDao.validate(p, null, raw, EncodingEnum.JSON, null, null, mySrd); + @Test + public void createStructDefThenValidatePatientWithRealUrl() throws IOException { + // setup + createStructureDefinitionInDao(); - String encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome.getOperationOutcome()); - ourLog.info("OO: {}", encoded); - assertThat(encoded, containsString("No issues detected")); + // execute + final String outcomePatientValidate = validate(PATIENT_WITH_REAL_URL); + + // verify + assertExpectedOutcome(outcomePatientValidate); + } + + @Test + public void validatePatientWithFakeUrlStructDefThenValidatePatientWithRealUrl() throws IOException { + // setup + final String outcomePatientValidateFakeUrl = validate(PATIENT_WITH_FAKE_URL); + assertTrue(outcomePatientValidateFakeUrl.contains(I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY)); + createStructureDefinitionInDao(); + + // execute + final String outcomePatientValidateRealUrl = validate(PATIENT_WITH_REAL_URL); + + // verify + assertExpectedOutcome(outcomePatientValidateRealUrl); + } + + @Test + public void validatePatientRealUrlThenCreateStructDefThenValidatePatientWithRealUrl() throws IOException { + // setup + final String outcomePatientValidateInitial = validate(PATIENT_WITH_REAL_URL); + assertTrue(outcomePatientValidateInitial.contains(I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY)); + createStructureDefinitionInDao(); + + // execute + final String outcomePatientValidateAfterStructDef = validate(PATIENT_WITH_REAL_URL); + + // verify + assertExpectedOutcome(outcomePatientValidateAfterStructDef); + } + + private static void assertExpectedOutcome(String outcomeJson) { + assertThat(outcomeJson, not(containsString(I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN_NOT_POLICY))); + assertThat(outcomeJson, containsString("No issues detected")); + } + + private String validate(Patient thePatient) { + final MethodOutcome validateOutcome = myPatientDao.validate(thePatient, null, myFhirContext.newJsonParser().encodeResourceToString(thePatient), EncodingEnum.JSON, null, null, mySrd); + return myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(validateOutcome.getOperationOutcome()); + } + + private void createStructureDefinitionInDao() throws IOException { + final StructureDefinition structureDefinition = loadResourceFromClasspath(StructureDefinition.class, "/r4/profile-differential-patient-r4.json"); + myStructureDefinitionDao.create(structureDefinition, new SystemRequestDetails()); + } + + private static Patient createPatient(String theUrl) { + final Patient patient = new Patient(); + patient.getText().setStatus(Narrative.NarrativeStatus.GENERATED); + patient.getText().getDiv().setValue("
hello
"); + patient.getMeta().addProfile(theUrl); + patient.setActive(true); + return patient; + } } @ParameterizedTest diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java index 49a714afd35..da64fee8df5 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java @@ -380,17 +380,11 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { observation.getEncounter().setReference(encounter.getId()); // not versioned builder.addTransactionCreateEntry(observation); - Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); - ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); - assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus()); - assertEquals("200 OK", outcome.getEntry().get(1).getResponse().getStatus()); - assertEquals("201 Created", outcome.getEntry().get(2).getResponse().getStatus()); + Bundle outcome = createAndValidateBundle((Bundle) builder.getBundle(), + List.of("200 OK", "200 OK", "201 Created"), List.of("2", "1", "1")); IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); IdType encounterId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); IdType observationId = new IdType(outcome.getEntry().get(2).getResponse().getLocation()); - assertEquals("2", patientId.getVersionIdPart()); - assertEquals("1", encounterId.getVersionIdPart()); - assertEquals("1", observationId.getVersionIdPart()); // Read back and verify that reference is now versioned observation = myObservationDao.read(observationId); @@ -429,14 +423,10 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { builder.addTransactionCreateEntry(observation); myCaptureQueriesListener.clear(); - Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); - ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); - assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus()); - assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus()); + Bundle outcome = createAndValidateBundle((Bundle) builder.getBundle(), + List.of("200 OK", "201 Created"), List.of("3", "1")); IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); - assertEquals("3", patientId.getVersionIdPart()); - assertEquals("1", observationId.getVersionIdPart()); // Make sure we're not introducing any extra DB operations assertEquals(3, myCaptureQueriesListener.logSelectQueries().size()); @@ -468,14 +458,10 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { myCaptureQueriesListener.clear(); - Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); - ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); - assertEquals("200 OK", outcome.getEntry().get(0).getResponse().getStatus()); - assertEquals("201 Created", outcome.getEntry().get(1).getResponse().getStatus()); + Bundle outcome = createAndValidateBundle((Bundle) builder.getBundle(), + List.of("200 OK", "201 Created"), List.of("3", "1")); IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); - assertEquals("3", patientId.getVersionIdPart()); - assertEquals("1", observationId.getVersionIdPart()); // Make sure we're not introducing any extra DB operations assertEquals(4, myCaptureQueriesListener.logSelectQueries().size()); @@ -563,20 +549,91 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { BundleBuilder builder = new BundleBuilder(myFhirContext); builder.addTransactionCreateEntry(messageHeader); - ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(builder.getBundle())); - Bundle outcome = mySystemDao.transaction(mySrd, (Bundle) builder.getBundle()); - ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); - assertEquals("201 Created", outcome.getEntry().get(0).getResponse().getStatus()); - + Bundle outcome = createAndValidateBundle((Bundle) builder.getBundle(), + List.of("201 Created"), List.of("1")); IdType messageHeaderId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); assertEquals("2", patient.getIdElement().getVersionIdPart()); - assertEquals("1", messageHeaderId.getVersionIdPart()); // read back and verify that reference is versioned messageHeader = myMessageHeaderDao.read(messageHeaderId); assertEquals(patient.getIdElement().getValue(), messageHeader.getFocus().get(0).getReference()); } + @Test + @DisplayName("#5619 Incorrect version of auto versioned reference for conditional update with urn id placeholder") + public void testInsertVersionedReferencesByPath_conditionalUpdateNoOpInTransaction_addsCorrectVersionToReference() { + Supplier supplier = () -> { + // create patient + Patient patient = new Patient(); + patient.setActive(true); + patient.addIdentifier().setSystem("http://example.com").setValue("test"); + + // add patient to the Bundle - conditional update with placeholder url + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry() + .setResource(patient) + .setFullUrl("urn:uuid:00001") + .getRequest() + .setMethod(Bundle.HTTPVerb.PUT) + .setUrl("Patient?identifier=http://example.com|test"); + + // create MessageHeader + MessageHeader messageHeader = new MessageHeader(); + messageHeader.getMeta().setExtension(messageHeaderAutoVersionExtension); + // add reference + messageHeader.addFocus().setReference("urn:uuid:00001"); + + bundle.addEntry() + .setResource(messageHeader) + .getRequest() + .setMethod(Bundle.HTTPVerb.POST) + .setUrl("/MessageHeader"); + + return bundle; + }; + + // create bundle first time + Bundle outcome = createAndValidateBundle(supplier.get(), + List.of("201 Created", "201 Created"), List.of("1", "1")); + IdType patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); + IdType messageHeaderId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); + + // read back and verify that reference is versioned and correct + Patient patient = myPatientDao.read(patientId); + MessageHeader messageHeader = myMessageHeaderDao.read(messageHeaderId); + assertEquals(patient.getIdElement().getValue(), messageHeader.getFocus().get(0).getReference()); + + // create bundle second time + outcome = createAndValidateBundle(supplier.get(), List.of("200 OK", "201 Created"), List.of("1", "1")); + patientId = new IdType(outcome.getEntry().get(0).getResponse().getLocation()); + messageHeaderId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); + + // read back and verify that reference is versioned and correct + patient = myPatientDao.read(patientId); + messageHeader = myMessageHeaderDao.read(messageHeaderId); + assertEquals(patient.getIdElement().getValue(), messageHeader.getFocus().get(0).getReference()); + } + + private Bundle createAndValidateBundle(Bundle theBundle, List theOutcomeStatuses, + List theOutcomeVersions) { + assertEquals(theBundle.getEntry().size(), theOutcomeStatuses.size(), + "Size of OutcomeStatuses list is incorrect"); + assertEquals(theBundle.getEntry().size(), theOutcomeVersions.size(), + "Size of OutcomeVersions list is incorrect"); + + ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(theBundle)); + Bundle result = mySystemDao.transaction(mySrd, theBundle); + ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(theBundle)); + + for (int i = 0; i < result.getEntry().size(); i++) { + assertEquals(theOutcomeStatuses.get(i), result.getEntry().get(i).getResponse().getStatus()); + IIdType resultId = new IdType(result.getEntry().get(i).getResponse().getLocation()); + assertEquals(theOutcomeVersions.get(i), resultId.getVersionIdPart()); + } + return result; + } + private Patient createAndUpdatePatient(String thePatientId) { Patient patient = new Patient(); patient.setId(thePatientId); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java index 84e8b9e5ef3..bee0810d226 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientCompartmentEnforcingInterceptorTest.java @@ -1,8 +1,8 @@ package ca.uhn.fhir.jpa.interceptor; import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; -import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.hl7.fhir.r4.model.Annotation; @@ -16,28 +16,26 @@ import org.springframework.beans.factory.annotation.Autowired; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -public class PatientCompartmentEnforcingInterceptorTest extends BaseJpaR4Test { +public class PatientCompartmentEnforcingInterceptorTest extends BaseResourceProviderR4Test { public static final int ALTERNATE_DEFAULT_ID = -1; - private PatientIdPartitionInterceptor mySvc; - private ForceOffsetSearchModeInterceptor myForceOffsetSearchModeInterceptor; - private PatientCompartmentEnforcingInterceptor myUpdateCrossPartitionInterceptor; - @Autowired private ISearchParamExtractor mySearchParamExtractor; + private ForceOffsetSearchModeInterceptor myForceOffsetSearchModeInterceptor; + private PatientIdPartitionInterceptor myPatientIdPartitionInterceptor; + private PatientCompartmentEnforcingInterceptor mySvc; @Override @BeforeEach public void before() throws Exception { super.before(); - mySvc = new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings); myForceOffsetSearchModeInterceptor = new ForceOffsetSearchModeInterceptor(); - myUpdateCrossPartitionInterceptor = new PatientCompartmentEnforcingInterceptor( - myFhirContext, mySearchParamExtractor); + myPatientIdPartitionInterceptor = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); + mySvc = new PatientCompartmentEnforcingInterceptor(getFhirContext(), mySearchParamExtractor); - myInterceptorRegistry.registerInterceptor(mySvc); + myInterceptorRegistry.registerInterceptor(myPatientIdPartitionInterceptor); myInterceptorRegistry.registerInterceptor(myForceOffsetSearchModeInterceptor); - myInterceptorRegistry.registerInterceptor(myUpdateCrossPartitionInterceptor); + myInterceptorRegistry.registerInterceptor(mySvc); myPartitionSettings.setPartitioningEnabled(true); myPartitionSettings.setUnnamedPartitionMode(true); @@ -46,9 +44,9 @@ public class PatientCompartmentEnforcingInterceptorTest extends BaseJpaR4Test { @AfterEach public void after() { - myInterceptorRegistry.unregisterInterceptor(mySvc); + myInterceptorRegistry.unregisterInterceptor(myPatientIdPartitionInterceptor); myInterceptorRegistry.unregisterInterceptor(myForceOffsetSearchModeInterceptor); - myInterceptorRegistry.unregisterInterceptor(myUpdateCrossPartitionInterceptor); + myInterceptorRegistry.unregisterInterceptor(mySvc); myPartitionSettings.setPartitioningEnabled(false); myPartitionSettings.setUnnamedPartitionMode(new PartitionSettings().isUnnamedPartitionMode()); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptorTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptorTest.java index 0e432bbcee5..d0e116343aa 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptorTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptorTest.java @@ -2,23 +2,27 @@ package ca.uhn.fhir.jpa.interceptor; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; -import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4SystemTest; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ResourceTable; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.MultimapCollector; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations; @@ -48,21 +52,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest { - +public class PatientIdPartitionInterceptorTest extends BaseResourceProviderR4Test { public static final int ALTERNATE_DEFAULT_ID = -1; - private PatientIdPartitionInterceptor mySvc; - private ForceOffsetSearchModeInterceptor myForceOffsetSearchModeInterceptor; @Autowired private ISearchParamExtractor mySearchParamExtractor; + private ForceOffsetSearchModeInterceptor myForceOffsetSearchModeInterceptor; + private PatientIdPartitionInterceptor mySvc; @Override @BeforeEach public void before() throws Exception { super.before(); - mySvc = new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings); myForceOffsetSearchModeInterceptor = new ForceOffsetSearchModeInterceptor(); + mySvc = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); myInterceptorRegistry.registerInterceptor(mySvc); myInterceptorRegistry.registerInterceptor(myForceOffsetSearchModeInterceptor); @@ -539,4 +542,15 @@ public class PatientIdPartitionInterceptorTest extends BaseJpaR4SystemTest { return (Patient)update.getResource(); } + @Test + public void testSystemOperation_withNoResourceType_success() throws IOException { + HttpPost post = new HttpPost(myServer.getBaseUrl() + "/" + ProviderConstants.OPERATION_EXPORT); + post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC); + + try (CloseableHttpResponse postResponse = myServer.getHttpClient().execute(post)){ + ourLog.info("Response: {}",postResponse); + assertEquals(202, postResponse.getStatusLine().getStatusCode()); + assertEquals("Accepted", postResponse.getStatusLine().getReasonPhrase()); + } + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java index fdad3b86b7d..ca4222ddb35 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheTest.java @@ -15,8 +15,6 @@ import ca.uhn.fhir.util.ClasspathUtil; import org.hl7.fhir.utilities.npm.NpmPackage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -30,8 +28,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; public class JpaPackageCacheTest extends BaseJpaR4Test { - - private static final Logger ourLog = LoggerFactory.getLogger(JpaPackageCacheTest.class); @Autowired private IHapiPackageCacheManager myPackageCacheManager; @Autowired @@ -45,6 +41,8 @@ public class JpaPackageCacheTest extends BaseJpaR4Test { @Autowired private ISearchParamExtractor mySearchParamExtractor; + private PatientIdPartitionInterceptor myPatientIdPartitionInterceptor; + @AfterEach public void disablePartitioning() { myPartitionSettings.setPartitioningEnabled(false); @@ -79,8 +77,8 @@ public class JpaPackageCacheTest extends BaseJpaR4Test { public void testSaveAndDeletePackagePartitionsEnabled() throws IOException { myPartitionSettings.setPartitioningEnabled(true); myPartitionSettings.setDefaultPartitionId(1); - PatientIdPartitionInterceptor patientIdPartitionInterceptor = new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings); - myInterceptorService.registerInterceptor(patientIdPartitionInterceptor); + myPatientIdPartitionInterceptor = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); + myInterceptorService.registerInterceptor(myPatientIdPartitionInterceptor); myInterceptorService.registerInterceptor(myRequestTenantPartitionInterceptor); try { try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/basisprofil.de.tar.gz")) { @@ -108,7 +106,7 @@ public class JpaPackageCacheTest extends BaseJpaR4Test { List deleteOutcomeMsgs = deleteOutcomeJson.getMessage(); assertEquals("Deleting package basisprofil.de#0.2.40", deleteOutcomeMsgs.get(0)); } finally { - myInterceptorService.unregisterInterceptor(patientIdPartitionInterceptor); + myInterceptorService.unregisterInterceptor(myPatientIdPartitionInterceptor); myInterceptorService.unregisterInterceptor(myRequestTenantPartitionInterceptor); } } @@ -119,8 +117,8 @@ public class JpaPackageCacheTest extends BaseJpaR4Test { myPartitionSettings.setDefaultPartitionId(0); boolean isUnnamed = myPartitionSettings.isUnnamedPartitionMode(); myPartitionSettings.setUnnamedPartitionMode(true); - PatientIdPartitionInterceptor patientIdPartitionInterceptor = new PatientIdPartitionInterceptor(myFhirContext, mySearchParamExtractor, myPartitionSettings); - myInterceptorService.registerInterceptor(patientIdPartitionInterceptor); + myPatientIdPartitionInterceptor = new PatientIdPartitionInterceptor(getFhirContext(), mySearchParamExtractor, myPartitionSettings); + myInterceptorService.registerInterceptor(myPatientIdPartitionInterceptor); myInterceptorService.registerInterceptor(myRequestTenantPartitionInterceptor); try { try (InputStream stream = ClasspathUtil.loadResourceAsStream("/packages/hl7.fhir.uv.shorthand-0.12.0.tgz")) { @@ -147,7 +145,7 @@ public class JpaPackageCacheTest extends BaseJpaR4Test { assertEquals("Deleting package hl7.fhir.uv.shorthand#0.12.0", deleteOutcomeMsgs.get(0)); } finally { myPartitionSettings.setUnnamedPartitionMode(isUnnamed); - myInterceptorService.unregisterInterceptor(patientIdPartitionInterceptor); + myInterceptorService.unregisterInterceptor(myPatientIdPartitionInterceptor); myInterceptorService.unregisterInterceptor(myRequestTenantPartitionInterceptor); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java index 9001bbcd925..69fa8ebbac2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider; import ca.uhn.fhir.jpa.delete.ThreadSafeResourceDeleterSvc; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.model.util.JpaConstants; @@ -14,6 +13,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; @@ -37,11 +37,13 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.ExplanationOfBenefit; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.MessageHeader; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.Organization; @@ -49,11 +51,14 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; 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 org.springframework.beans.factory.annotation.Autowired; @@ -68,7 +73,9 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Test { @@ -79,6 +86,8 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes private SearchParamMatcher mySearchParamMatcher; @Autowired private ThreadSafeResourceDeleterSvc myThreadSafeResourceDeleterSvc; + private AuthorizationInterceptor myReadAllBundleInterceptor; + private AuthorizationInterceptor myReadAllPatientInterceptor; @BeforeEach @Override @@ -87,7 +96,8 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes myStorageSettings.setAllowMultipleDelete(true); myStorageSettings.setExpungeEnabled(true); myStorageSettings.setDeleteExpungeEnabled(true); - myServer.getRestfulServer().registerInterceptor(new BulkDataExportProvider()); + myReadAllBundleInterceptor = new ReadAllAuthorizationInterceptor("Bundle"); + myReadAllPatientInterceptor = new ReadAllAuthorizationInterceptor("Patient"); } @Override @@ -1506,4 +1516,250 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes } } + + @Test + public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForDocumentBundles(){ + myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor); + + Bundle bundle1 = createDocumentBundle(createPatient("John", "Smith")); + Bundle bundle2 = createDocumentBundle(createPatient("Jane", "Doe")); + assertSearchContainsResources("/Bundle", bundle1, bundle2); + } + + @Test + public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForCollectionBundles(){ + myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor); + + Bundle bundle1 = createCollectionBundle(createPatient("John", "Smith")); + Bundle bundle2 = createCollectionBundle(createPatient("Jane", "Doe")); + assertSearchContainsResources("/Bundle", bundle1, bundle2); + } + + @Test + public void testSearchBundles_withPermissionToSearchAllBundles_doesNotReturn403ForbiddenForMessageBundles(){ + myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor); + + Bundle bundle1 = createMessageHeaderBundle(createPatient("John", "Smith")); + Bundle bundle2 = createMessageHeaderBundle(createPatient("Jane", "Doe")); + assertSearchContainsResources("/Bundle", bundle1, bundle2); + } + + @Test + public void testSearchBundles_withPermissionToViewOneBundle_onlyAllowsViewingOneBundle(){ + Bundle bundle1 = createMessageHeaderBundle(createPatient("John", "Smith")); + Bundle bundle2 = createMessageHeaderBundle(createPatient("Jane", "Doe")); + + myServer.getRestfulServer().getInterceptorService().registerInterceptor( + new ReadInCompartmentAuthorizationInterceptor("Bundle", bundle1.getIdElement()) + ); + + assertSearchContainsResources("/Bundle?_id=" + bundle1.getIdPart(), bundle1); + assertSearchFailsWith403Forbidden("/Bundle?_id=" + bundle2.getIdPart()); + assertSearchFailsWith403Forbidden("/Bundle"); + } + + @Test + public void testSearchPatients_withPermissionToSearchAllBundles_returns403Forbidden(){ + myServer.getRestfulServer().registerInterceptor(myReadAllBundleInterceptor); + + createPatient("John", "Smith"); + createPatient("Jane", "Doe"); + assertSearchFailsWith403Forbidden("/Patient"); + } + + @Test + public void testSearchPatients_withPermissionToSearchAllPatients_returnsAllPatients(){ + myServer.getRestfulServer().registerInterceptor(myReadAllPatientInterceptor); + + Patient patient1 = createPatient("John", "Smith"); + Patient patient2 = createPatient("Jane", "Doe"); + assertSearchContainsResources("/Patient", patient1, patient2); + } + + @Test + public void testSearchPatients_withPermissionToViewOnePatient_onlyAllowsViewingOnePatient(){ + Patient patient1 = createPatient("John", "Smith"); + Patient patient2 = createPatient("Jane", "Doe"); + + myServer.getRestfulServer().getInterceptorService().registerInterceptor( + new ReadInCompartmentAuthorizationInterceptor("Patient", patient1.getIdElement()) + ); + + assertSearchContainsResources("/Patient?_id=" + patient1.getIdPart(), patient1); + assertSearchFailsWith403Forbidden("/Patient?_id=" + patient2.getIdPart()); + assertSearchFailsWith403Forbidden("/Patient"); + } + + @Test + public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingDocumentBundles_onlyRecursesOneLevelDeep() { + Bundle bundle1 = createDocumentBundle(createPatient("John", "Smith")); + Bundle bundle2 = createDocumentBundle(createPatient("John", "Smith")); + Bundle searchSet = createSearchSet(bundle1, bundle2); + + RequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setResourceName("Bundle"); + + List resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(requestDetails, searchSet, myFhirContext); + assertEquals(2, resources.size()); + assertTrue(resources.contains(bundle1)); + assertTrue(resources.contains(bundle2)); + } + + @Test + public void testToListOfResourcesAndExcludeContainer_withSearchSetContainingPatients_returnsPatients() { + Patient patient1 = createPatient("John", "Smith"); + Patient patient2 = createPatient("Jane", "Doe"); + Bundle searchSet = createSearchSet(patient1, patient2); + + RequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setResourceName("Patient"); + + List resources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(requestDetails, searchSet, myFhirContext); + assertEquals(2, resources.size()); + assertTrue(resources.contains(patient1)); + assertTrue(resources.contains(patient2)); + } + + @ParameterizedTest + @EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "COLLECTION", "MESSAGE"}) + public void testShouldExamineBundleResources_withBundleRequestAndStandAloneBundleType_returnsFalse(Bundle.BundleType theBundleType){ + RequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setResourceName("Bundle"); + Bundle bundle = new Bundle(); + bundle.setType(theBundleType); + + assertFalse(AuthorizationInterceptor.shouldExamineBundleChildResources(requestDetails, myFhirContext, bundle)); + } + + @ParameterizedTest + @EnumSource(value = Bundle.BundleType.class, names = {"DOCUMENT", "COLLECTION", "MESSAGE"}, mode= EnumSource.Mode.EXCLUDE) + public void testShouldExamineBundleResources_withBundleRequestAndNonStandAloneBundleType_returnsTrue(Bundle.BundleType theBundleType){ + RequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setResourceName("Bundle"); + Bundle bundle = new Bundle(); + bundle.setType(theBundleType); + + assertTrue(AuthorizationInterceptor.shouldExamineBundleChildResources(requestDetails, myFhirContext, bundle)); + } + + @ParameterizedTest + @EnumSource(value = Bundle.BundleType.class) + public void testShouldExamineBundleResources_withNonBundleRequests_returnsTrue(Bundle.BundleType theBundleType){ + RequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setResourceName("Patient"); + Bundle bundle = new Bundle(); + bundle.setType(theBundleType); + + assertTrue(AuthorizationInterceptor.shouldExamineBundleChildResources(requestDetails, myFhirContext, bundle)); + } + + private Patient createPatient(String theFirstName, String theLastName){ + Patient patient = new Patient(); + patient.addName().addGiven(theFirstName).setFamily(theLastName); + return (Patient) myPatientDao.create(patient, mySrd).getResource(); + } + + private Bundle createDocumentBundle(Patient thePatient){ + Composition composition = new Composition(); + composition.setType(new CodeableConcept().addCoding(new Coding().setSystem("http://example.org").setCode("some-type"))); + composition.getSubject().setReference(thePatient.getIdElement().getValue()); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + bundle.addEntry().setResource(composition); + bundle.addEntry().setResource(thePatient); + return (Bundle) myBundleDao.create(bundle, mySrd).getResource(); + } + + private Bundle createCollectionBundle(Patient thePatient) { + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.COLLECTION); + bundle.addEntry().setResource(thePatient); + return (Bundle) myBundleDao.create(bundle, mySrd).getResource(); + } + + private Bundle createMessageHeaderBundle(Patient thePatient) { + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.MESSAGE); + + MessageHeader messageHeader = new MessageHeader(); + Coding event = new Coding().setSystem("http://acme.com").setCode("some-event"); + messageHeader.setEvent(event); + messageHeader.getFocusFirstRep().setReference(thePatient.getIdElement().getValue()); + bundle.addEntry().setResource(messageHeader); + bundle.addEntry().setResource(thePatient); + + return (Bundle) myBundleDao.create(bundle, mySrd).getResource(); + } + + private void assertSearchContainsResources(String theUrl, Resource... theExpectedResources){ + List expectedIds = Arrays.stream(theExpectedResources) + .map(resource -> resource.getIdPart()) + .toList(); + + Bundle searchResult = myClient + .search() + .byUrl(theUrl) + .returnBundle(Bundle.class) + .execute(); + + List actualIds = searchResult.getEntry().stream() + .map(entry -> entry.getResource().getIdPart()) + .toList(); + + assertEquals(expectedIds.size(), actualIds.size()); + assertTrue(expectedIds.containsAll(actualIds)); + } + + private void assertSearchFailsWith403Forbidden(String theUrl){ + try { + myClient.search().byUrl(theUrl).execute(); + fail(); + } catch (Exception e){ + assertTrue(e.getMessage().contains("HTTP 403 Forbidden")); + } + } + + private Bundle createSearchSet(Resource... theResources){ + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.SEARCHSET); + Arrays.stream(theResources).forEach(resource -> bundle.addEntry().setResource(resource)); + return bundle; + } + + static class ReadAllAuthorizationInterceptor extends AuthorizationInterceptor { + + private final String myResourceType; + + public ReadAllAuthorizationInterceptor(String theResourceType){ + super(PolicyEnum.DENY); + myResourceType = theResourceType; + } + + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().read().resourcesOfType(myResourceType).withAnyId().andThen() + .build(); + } + } + + static class ReadInCompartmentAuthorizationInterceptor extends AuthorizationInterceptor { + + private final String myResourceType; + private final IIdType myId; + + public ReadInCompartmentAuthorizationInterceptor(String theResourceType, IIdType theId){ + super(PolicyEnum.DENY); + myResourceType = theResourceType; + myId = theId; + } + + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().read().allResources().inCompartment(myResourceType, myId).andThen() + .build(); + } + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/BulkExportProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/BulkExportProviderR4Test.java index d0a1c576f1c..dd1a0d99f5f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/BulkExportProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/BulkExportProviderR4Test.java @@ -43,8 +43,6 @@ public class BulkExportProviderR4Test extends BaseResourceProviderR4Test { assertThat(e.getStatusCode(), equalTo(404)); } - - @Test void testBulkExport_typePatientIdNotExists_throws404() { // given no data diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4DistanceTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4DistanceTest.java index 1c6e5858820..8c27583cf0c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4DistanceTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4DistanceTest.java @@ -1,14 +1,24 @@ package ca.uhn.fhir.jpa.provider.r4; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.search.builder.QueryStack; import ca.uhn.fhir.jpa.util.CoordCalculatorTestUtil; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import ca.uhn.fhir.rest.gclient.ICriterion; +import ca.uhn.fhir.rest.gclient.ICriterionInternal; +import ca.uhn.fhir.rest.gclient.IParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Location; import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.SearchParameter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -16,12 +26,19 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; public class ResourceProviderR4DistanceTest extends BaseResourceProviderR4Test { @@ -345,6 +362,242 @@ public class ResourceProviderR4DistanceTest extends BaseResourceProviderR4Test { } } + + + + @Test + void shouldSortPractitionerRolesByLocationNear() { + + double latitude = 43.65513; + double longitude = -79.4173869; + double distance = 100.0; // km + + Location toronto = createLocationWithName("Toronto", 43.70, -79.42); + Location mississauga = createLocationWithName("Mississauga", 43.59, -79.64); + Location hamilton = createLocationWithName("Hamilton", 43.26, -79.87); + Location kitchener = createLocationWithName("Kitchener", 43.45, -80.49); + Location stCatharines = createLocationWithName("St. Catharines", 43.16, -79.24); + Location oshawa = createLocationWithName("Oshawa", 43.92, -78.86); + Location ottawa = createLocationWithName("Ottawa", 45.42, -75.69); + Location london = createLocationWithName("London", 42.98, -81.25); + Location barrie = createLocationWithName("Barrie", 44.39, -79.69); + Location windsor = createLocationWithName("Windsor", 42.31, -83.04); + + createPractitionerRole(toronto); + createPractitionerRole(ottawa); + createPractitionerRole(mississauga); + createPractitionerRole(hamilton); + createPractitionerRole(kitchener); + createPractitionerRole(london); + createPractitionerRole(stCatharines); + createPractitionerRole(oshawa); + createPractitionerRole(windsor); + createPractitionerRole(barrie); + + Bundle sortedPractitionerRoles = (Bundle) myClient.search() + .forResource(PractitionerRole.class) + .where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km")) + .sort(new SortSpec("location.near", SortOrderEnum.ASC)) + .execute(); + + List list = sortedPractitionerRoles.getEntry() + .stream() + .map(entry -> (PractitionerRole) entry.getResource()) + .flatMap(practitionerRole -> practitionerRole.getIdentifier().stream()) + .filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem())) + .map(Identifier::getValue).toList(); + + List referenceList = Arrays.asList( + "PractitionerRole-Toronto", + "PractitionerRole-Mississauga", + "PractitionerRole-St. Catharines", + "PractitionerRole-Oshawa", + "PractitionerRole-Hamilton", + "PractitionerRole-Barrie", + "PractitionerRole-Kitchener"); + + assertArrayEquals(referenceList.toArray(), list.toArray()); + + Bundle sortedPractitionerRolesDesc = (Bundle) myClient.search() + .forResource(PractitionerRole.class) + .where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km")) + .sort(new SortSpec("location.near", SortOrderEnum.DESC)) + .execute(); + + list = sortedPractitionerRolesDesc.getEntry() + .stream() + .map(entry -> (PractitionerRole) entry.getResource()) + .flatMap(practitionerRole -> practitionerRole.getIdentifier().stream()) + .filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem())) + .map(Identifier::getValue).toList(); + + Collections.reverse(referenceList); + assertArrayEquals(referenceList.toArray(), list.toArray()); + + } + + @Test + void shouldSortPractitionerRoleByLocationNameAndThenByLocationNearInSortChain() { + + double latitude = 56.15392798473292; + double longitude = 10.214247324883443; + double distance = 1000.0; // km + + + Location city1 = createLocationWithName("city", 56.4572068307235, 10.03257493847164); // randers + Location city2 = createLocationWithName("city", 55.37805615936569, 10.373173394141986); // odense + Location city3 = createLocationWithName("city", 57.03839389334237, 9.897971178848938); // aalborg + Location city4 = createLocationWithName("city", 53.59504499156986, 9.94180650612504); // hamburg + Location capital1 = createLocationWithName("capital", 55.67252420131149, 12.521336649310285); // copenhagen + Location capital2 = createLocationWithName("capital", 59.91879265293977, 10.743073107764332); // oslo + Location capital3 = createLocationWithName("capital", 51.53542091927589, -0.1535161240530497); // london + + createPractitionerRole(city1, "city1"); + createPractitionerRole(city2, "city2"); + createPractitionerRole(city3, "city3"); + createPractitionerRole(city4, "city4"); + createPractitionerRole(capital1, "capital1"); + createPractitionerRole(capital2, "capital2"); + createPractitionerRole(capital3, "capital3"); + + Bundle sortedPractitionerRoles = (Bundle) myClient.search() + .forResource(PractitionerRole.class) + .where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km")) + .sort(new SortSpec("location.name", SortOrderEnum.ASC, new SortSpec("location.near", SortOrderEnum.ASC))) + .execute(); + + List sortedValues = sortedPractitionerRoles + .getEntry() + .stream() + .map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier() + .stream() + .filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem())) + .map(Identifier::getValue) + .collect(Collectors.toList())) + .flatMap(List::stream) + .toList(); + + List referenceList = Arrays.asList( + "PractitionerRole-capital1", + "PractitionerRole-capital2", + "PractitionerRole-capital3", + "PractitionerRole-city1", + "PractitionerRole-city2", + "PractitionerRole-city3", + "PractitionerRole-city4"); + + assertArrayEquals(referenceList.toArray(), sortedValues.toArray()); + + Bundle sortedPractitionerRolesDesc = (Bundle) myClient.search() + .forResource(PractitionerRole.class) + .where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km")) + .sort(new SortSpec("location.name", SortOrderEnum.ASC, new SortSpec("location.near", SortOrderEnum.DESC))) + .execute(); + + List sortedValuesDesc = sortedPractitionerRolesDesc + .getEntry() + .stream() + .map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier() + .stream() + .filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem())) + .map(Identifier::getValue) + .collect(Collectors.toList())) + .flatMap(List::stream) + .toList(); + + referenceList = Arrays.asList( + "PractitionerRole-capital3", + "PractitionerRole-capital2", + "PractitionerRole-capital1", + "PractitionerRole-city4", + "PractitionerRole-city3", + "PractitionerRole-city2", + "PractitionerRole-city1" + ); + assertArrayEquals(referenceList.toArray(), sortedValuesDesc.toArray()); + } + + @Test + void shouldSortPractitionerRoleByLocationNearAndThenByLocationNameInSortChain() { + + double latitude = 56.15392798473292; + double longitude = 10.214247324883443; + double distance = 1000.0; // km + + createPractitionerRole(createLocationWithName("a-close-city", 56.4572068307235, 10.03257493847164)); + createPractitionerRole(createLocationWithName("b-close-city", 56.4572068307235, 10.03257493847164)); + createPractitionerRole(createLocationWithName("c-close-city", 56.4572068307235, 10.03257493847164)); + createPractitionerRole(createLocationWithName("x-far-city", 51.53542091927589, -0.1535161240530497)); + createPractitionerRole(createLocationWithName("y-far-city", 51.53542091927589, -0.1535161240530497)); + createPractitionerRole(createLocationWithName("z-far-city", 51.53542091927589, -0.1535161240530497)); + + Bundle sortedPractitionerRoles = (Bundle) myClient.search() + .forResource(PractitionerRole.class) + .where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km")) + .sort(new SortSpec("location.near", SortOrderEnum.ASC, new SortSpec("location.name"))) + .execute(); + + List sortedValues = sortedPractitionerRoles + .getEntry() + .stream() + .map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier() + .stream() + .filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem())) + .map(Identifier::getValue) + .collect(Collectors.toList())) + .flatMap(List::stream) + .toList(); + + assertArrayEquals( + Arrays.asList( + "PractitionerRole-a-close-city", + "PractitionerRole-b-close-city", + "PractitionerRole-c-close-city", + "PractitionerRole-x-far-city", + "PractitionerRole-y-far-city", + "PractitionerRole-z-far-city" + ).toArray(), sortedValues.toArray()); + + Bundle sortedPractitionerRolesDesc = (Bundle) myClient.search() + .forResource(PractitionerRole.class) + .where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km")) + .sort(new SortSpec("location.near", SortOrderEnum.DESC, new SortSpec("location.name"))) + .execute(); + + List sortedValuesDesc = sortedPractitionerRolesDesc + .getEntry() + .stream() + .map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier() + .stream() + .filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem())) + .map(Identifier::getValue) + .collect(Collectors.toList())) + .flatMap(List::stream) + .toList(); + + + assertArrayEquals( + Arrays.asList( + "PractitionerRole-x-far-city", + "PractitionerRole-y-far-city", + "PractitionerRole-z-far-city", + "PractitionerRole-a-close-city", + "PractitionerRole-b-close-city", + "PractitionerRole-c-close-city" + ).toArray(), sortedValuesDesc.toArray()); + } + + + @Test + void shouldThrowExceptionWhenSortingByChainedNearWithoutProvidingNearValue() { + assertThrows(InvalidRequestException.class, () -> { + myClient.search() + .forResource(PractitionerRole.class) + .sort(new SortSpec("location.near", SortOrderEnum.ASC)) + .execute(); + }, "HTTP 400 : HAPI-2307: Can not sort on coordinate parameter \"location\" unless this parameter is also specified as a search parameter with a latitude/longitude value"); + } + private void createLocation(String id, double latitude, double longitude) { Location loc = new Location(); loc.setId(id); @@ -354,5 +607,68 @@ public class ResourceProviderR4DistanceTest extends BaseResourceProviderR4Test { myLocationDao.update(loc, mySrd); } + public static final String PRACTITIONER_ROLE_SYSTEM = "http://api.someSystem.com/PractitionerRole"; + + + private Location createLocationWithName(String locationName, double latitude, double longitude) { + Location location = new Location(); + location.setStatus(Location.LocationStatus.ACTIVE); + location.setName(locationName); + location.addIdentifier() + .setSystem("http://api.someSystem.com/Location") + .setValue("TestLocation-" + locationName + "-" + UUID.randomUUID()); + location.getPosition().setLatitude(new BigDecimal(latitude)); + location.getPosition().setLongitude(new BigDecimal(longitude)); + + return doCreateResourceAndReturnInstance(location); + } + + private void createPractitionerRole(Location location, String city) { + PractitionerRole practitionerRole = new PractitionerRole(); + practitionerRole.setActive(true); + practitionerRole.addLocation(new Reference(location)); + practitionerRole.addIdentifier() + .setSystem(PRACTITIONER_ROLE_SYSTEM) + .setValue("PractitionerRole-" + city); + + doCreateResourceAndReturnInstance(practitionerRole); + } + + private void createPractitionerRole(Location location) { + PractitionerRole practitionerRole = new PractitionerRole(); + practitionerRole.setActive(true); + practitionerRole.addLocation(new Reference(location)); + practitionerRole.addIdentifier() + .setSystem(PRACTITIONER_ROLE_SYSTEM) + .setValue("PractitionerRole-" + location.getName()); + + doCreateResourceAndReturnInstance(practitionerRole); + } + + public T doCreateResourceAndReturnInstance(IBaseResource theResource) { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); + return (T) dao.create(theResource, mySrd).getResource(); + } + + + static class RawSearchCriterion implements ICriterion, ICriterionInternal { + private final String name; + private final String value; + + RawSearchCriterion(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String getParameterValue(FhirContext theContext) { + return value; + } + + @Override + public String getParameterName() { + return name; + } + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 93cfeeb1e0d..9963b436ba8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -26,13 +26,11 @@ import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.StrictErrorHandler; -import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.SearchTotalModeEnum; import ca.uhn.fhir.rest.api.SummaryEnum; -import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.client.apache.ResourceEntity; import ca.uhn.fhir.rest.client.api.IClientInterceptor; @@ -45,7 +43,6 @@ import ca.uhn.fhir.rest.gclient.NumberClientParam; import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ParamPrefixEnum; -import ca.uhn.fhir.rest.server.IPagingProvider; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; @@ -58,6 +55,7 @@ import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; import com.google.common.collect.Lists; +import jakarta.annotation.Nonnull; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -116,7 +114,9 @@ import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.ImagingStudy; import org.hl7.fhir.r4.model.InstantType; import org.hl7.fhir.r4.model.Location; @@ -167,14 +167,12 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Spy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.AopTestUtils; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; -import jakarta.annotation.Nonnull; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -191,7 +189,6 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -227,8 +224,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @SuppressWarnings("Duplicates") @@ -7423,4 +7418,49 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { ); } } + @Nested + class SearchWithIdentifiers { + private static final String SYSTEM = "http://acme.org/fhir/identifier/mrn"; + private static final String VALUE = "123456"; + private IIdType myPatientId; + + @BeforeEach + void beforeEach() { + final Patient patient = new Patient(); + patient.addIdentifier().setValue(VALUE).setSystem(SYSTEM); + patient.addName().setUse(HumanName.NameUse.OFFICIAL).setFamily("Abbott").addGiven("Elias").addPrefix("Mr."); + patient.setGender(Enumerations.AdministrativeGender.MALE); + + myPatientId = myClient.create().resource(patient).execute().getResource().getIdElement(); + + final Observation observation = new Observation(); + observation.setStatus(Observation.ObservationStatus.FINAL); + observation.setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://loinc.org").setCode("15074-8").setDisplay("Glucose [Moles/volume] in Blood"))); + observation.setSubject(new Reference(myPatientId.toUnqualifiedVersionless()).setIdentifier(new Identifier().setSystem(SYSTEM).setValue(VALUE))); + + myClient.create().resource(observation).execute().getResource().getIdElement(); + } + + @Test + void searchWithIdentifierToIdentifier() { + testAndAssertFailureFor("Observation?subject:identifier=http://acme.org/fhir/identifier/mrn|123456"); + } + + @Test + void searchWithIdentifierToId() { + testAndAssertFailureFor(String.format("Observation?subject:identifier=%s", myPatientId.getIdPart())); + } + + private void testAndAssertFailureFor(String theUrl) { + try { + myClient.search() + .byUrl(theUrl) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (InvalidRequestException exception) { + assertEquals("HTTP 400 Bad Request: HAPI-2498: Unsupported search modifier(s): \"[:identifier]\" for resource type \"Observation\". Valid search modifiers are: [:contains, :exact, :in, :iterate, :missing, :not-in, :of-type, :recurse, :text]", exception.getMessage()); + } + } + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java index 2ab6dd76bf7..a3e9614ff32 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java @@ -202,7 +202,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv myLocalVs.setUrl(URL_MY_VALUE_SET); ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(codeSystem.getUrl()); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } @@ -1199,7 +1199,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .setSystem(URL_MY_CODE_SYSTEM) .addFilter() .setProperty("concept") - .setOp(FilterOperator.ISA) + .setOp(FilterOperator.DESCENDENTOF) .setValue("A"); myLocalVs .getCompose() diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java index 7b56df6dbc8..cb8de80537b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java @@ -167,7 +167,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid myLocalVs.setUrl(URL_MY_VALUE_SET); ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(codeSystem.getUrl()); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java index cc8d09d8ff7..f10efb5b1f6 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java @@ -196,7 +196,7 @@ public class ResourceProviderR4ValueSetVerCSVerTest extends BaseResourceProvider ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(theCodeSystemUrl); include.setVersion(theValueSetVersion); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); return myLocalVs; } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java index ec9d5d8a4d5..dcce168fa09 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java @@ -17,8 +17,10 @@ import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl; import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl; import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap; import ca.uhn.fhir.jpa.cache.ResourceVersionMap; +import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; import ca.uhn.fhir.jpa.dao.JpaResourceDao; +import ca.uhn.fhir.jpa.dao.ResourceHistoryCalculator; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; @@ -148,6 +150,7 @@ public class GiantTransactionPerfTest { private IIdHelperService myIdHelperService; @Mock private IJpaStorageResourceParser myJpaStorageResourceParser; + private final ResourceHistoryCalculator myResourceHistoryCalculator = new ResourceHistoryCalculator(FhirContext.forR4Cached(), false); private IMetaTagSorter myMetaTagSorter; @AfterEach @@ -271,6 +274,7 @@ public class GiantTransactionPerfTest { myEobDao.setJpaStorageResourceParserForUnitTest(myJpaStorageResourceParser); myEobDao.setExternallyStoredResourceServiceRegistryForUnitTest(new ExternallyStoredResourceServiceRegistry()); myEobDao.setMyMetaTagSorter(myMetaTagSorter); + myEobDao.setResourceHistoryCalculator(myResourceHistoryCalculator); myEobDao.start(); myDaoRegistry.setResourceDaos(Lists.newArrayList(myEobDao)); diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index 6caf9b57ee3..f7fefdee4a1 100644 --- a/hapi-fhir-jpaserver-test-r4b/pom.xml +++ b/hapi-fhir-jpaserver-test-r4b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/pom.xml b/hapi-fhir-jpaserver-test-r5/pom.xml index 589514a0920..36d61f96d9b 100644 --- a/hapi-fhir-jpaserver-test-r5/pom.xml +++ b/hapi-fhir-jpaserver-test-r5/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/CrossPartitionReferencesTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/CrossPartitionReferencesTest.java index c1d6bb2e381..9b6d440bbfa 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/CrossPartitionReferencesTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/CrossPartitionReferencesTest.java @@ -19,6 +19,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r5.model.Enumerations; @@ -34,11 +35,8 @@ import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Propagation; -import jakarta.annotation.Nonnull; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -194,8 +192,7 @@ public class CrossPartitionReferencesTest extends BaseJpaR5Test { when(myCrossPartitionReferencesDetectedInterceptor.handle(any(),any())).thenAnswer(t->{ CrossPartitionReferenceDetails theDetails = t.getArgument(1, CrossPartitionReferenceDetails.class); IIdType targetId = theDetails.getPathAndRef().getRef().getReferenceElement(); - ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forRead(targetId); - RequestPartitionId referenceTargetPartition = myPartitionHelperSvc.determineReadPartitionForRequest(theDetails.getRequestDetails(), details); + RequestPartitionId referenceTargetPartition = myPartitionHelperSvc.determineReadPartitionForRequestForRead(theDetails.getRequestDetails(), targetId.getResourceType(), targetId); IResourceLookup targetResource = myTransactionService .withRequest(theDetails.getRequestDetails()) diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoTransactionR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoTransactionR5Test.java index 75bc00c23c9..f5d7992b1fc 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoTransactionR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoTransactionR5Test.java @@ -13,6 +13,7 @@ import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.Quantity; import org.hl7.fhir.r5.model.Reference; +import org.hl7.fhir.r5.model.UriType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -27,6 +28,8 @@ import static org.apache.commons.lang3.StringUtils.countMatches; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -670,6 +673,30 @@ public class FhirSystemDaoTransactionR5Test extends BaseJpaR5Test { } + /** + * See #5110 + */ + @Test + public void testTransactionWithMissingSystem() { + BundleBuilder bb = new BundleBuilder(myFhirContext); + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + + // The identifier has a system URI that has no value, only an extension + UriType system = new UriType(); + system.addExtension("http://hl7.org/fhir/StructureDefinition/data-absent-reason", new CodeType("unknown")); + patient.addIdentifier().setValue("m123").setSystemElement(system); + + patient.addName().setText("Jane Doe"); + bb.addTransactionCreateEntry(patient); + Bundle inputBundle = bb.getBundleTyped(); + + Bundle outputBundle = mySystemDao.transaction(mySrd, inputBundle); + + assertThat(outputBundle.getEntry().get(0).getResponse().getLocation(), matchesPattern("Patient/[0-9]+/_history/1")); + } + + @Nonnull private static Bundle createBundleWithConditionalDeleteAndConditionalUpdateOnSameResource(FhirContext theFhirContext) { // Build a new bundle each time we need it diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java index f5f882e38ff..9df23b88314 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java @@ -208,7 +208,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { myLocalVs.setUrl(URL_MY_VALUE_SET); ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(codeSystem.getUrl()); - include.addFilter().setProperty("concept").setOp(Enumerations.FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(Enumerations.FilterOperator.DESCENDENTOF).setValue("ParentA"); myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java index 824384e57c4..c7af44cab70 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java @@ -231,7 +231,7 @@ public class ResourceProviderR5ValueSetVersionedTest extends BaseResourceProvide ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(theCodeSystemUrl); include.setVersion(theValueSetVersion); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.DESCENDENTOF).setValue("ParentA"); return myLocalVs; } diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 16b27f292e2..98f6c8f2f3b 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/auth/RuleImplOpTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/auth/RuleImplOpTest.java new file mode 100644 index 00000000000..0bfb332f580 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/auth/RuleImplOpTest.java @@ -0,0 +1,135 @@ +package ca.uhn.fhir.jpa.auth; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule; +import ca.uhn.fhir.rest.server.interceptor.auth.IRuleApplier; +import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum; +import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder; +import ca.uhn.fhir.util.BundleBuilder; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class RuleImplOpTest { + private static final String OPERATION = "operation"; + private static final String TYPE = "type"; + private static final String PATH = "path"; + private static final String VALUE = "value"; + private static final String REPLACE = "replace"; + private static final String PATIENT_BIRTH_DATE = "Patient.birthDate"; + private static final Parameters PARAMETERS = buildParameters(); + private static final String DOCUMENT = "document"; + private static final String ERROR_TEMPLATE = "HAPI-0339: Can not handle transaction with nested resource of type %s"; + private static final String ERROR_PARAMETERS = String.format(ERROR_TEMPLATE, "Parameters"); + private static final String ERROR_BUNDLE = String.format(ERROR_TEMPLATE, "Bundle"); + + private static final String REQUEST_RULELIST = AuthorizationInterceptor.class.getName() + "_1_RULELIST"; + private final Patient myPatient = buildPatient(); + + private final List myRules = new RuleBuilder() + .allow() + .transaction() + .withAnyOperation() + .andApplyNormalRules() + .andThen() + .allow() + .write() + .allResources() + .withAnyId() + .build(); + + private final IAuthRule myRule = myRules.get(0); + private final FhirContext myFhirContext = FhirContext.forR4Cached(); + private final IBaseBundle myInnerBundle = buildInnerBundler(myFhirContext); + + private final RequestDetails mySystemRequestDetails = buildSystemRequestDetails(myFhirContext, myRules); + private final IRuleApplier myRuleApplier = new AuthorizationInterceptor(); + + @Test + void testTransactionBundleUpdateWithParameters() { + final BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + bundleBuilder.addTransactionUpdateEntry(PARAMETERS); + + try { + applyRule(bundleBuilder.getBundle()); + fail("Expected an InvalidRequestException"); + } catch (InvalidRequestException exception) { + assertEquals(ERROR_PARAMETERS, exception.getMessage()); + } + } + + @Test + void testTransactionBundleWithNestedBundle() { + final BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + bundleBuilder.addTransactionCreateEntry(myInnerBundle); + + try { + applyRule(bundleBuilder.getBundle()); + fail("Expected an InvalidRequestException"); + } catch (InvalidRequestException exception) { + assertEquals(ERROR_BUNDLE, exception.getMessage()); + } + } + + @Test + void testTransactionBundlePatchWithParameters() { + final BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + bundleBuilder.addTransactionFhirPatchEntry(myPatient.getIdElement(), PARAMETERS); + + final AuthorizationInterceptor.Verdict verdict = applyRule(bundleBuilder.getBundle()); + + assertThat(verdict.getDecision(), equalTo(PolicyEnum.ALLOW)); + } + + private AuthorizationInterceptor.Verdict applyRule(IBaseBundle theBundle) { + return myRule.applyRule(RestOperationTypeEnum.TRANSACTION, mySystemRequestDetails, theBundle, myPatient.getIdElement(), myPatient, myRuleApplier, new HashSet<>(), null); + } + + private static Parameters buildParameters() { + final Parameters patch = new Parameters(); + + final Parameters.ParametersParameterComponent op = patch.addParameter().setName(OPERATION); + op.addPart().setName(TYPE).setValue(new CodeType(REPLACE)); + op.addPart().setName(PATH).setValue(new CodeType(PATIENT_BIRTH_DATE)); + op.addPart().setName(VALUE).setValue(new StringType("1912-04-14")); + + return patch; + } + + private static RequestDetails buildSystemRequestDetails(FhirContext theFhirContext, List theRules) { + final SystemRequestDetails systemRequestDetails = new SystemRequestDetails(); + systemRequestDetails.setFhirContext(theFhirContext); + systemRequestDetails.getUserData().put(REQUEST_RULELIST, theRules); + + return systemRequestDetails; + } + + private static Patient buildPatient() { + final Patient patient = new Patient(); + patient.setId(new IdType("Patient", "1")); + return patient; + } + + private static IBaseBundle buildInnerBundler(FhirContext theFhirContext) { + final BundleBuilder innerBundleBuilder = new BundleBuilder(theFhirContext); + innerBundleBuilder.setType(DOCUMENT); + return innerBundleBuilder.getBundle(); + } +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunnerTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunnerTest.java index 4fa1dcf9ad3..73dff282646 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunnerTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunnerTest.java @@ -18,7 +18,8 @@ import java.util.Set; import java.util.function.Consumer; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.oneOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -98,10 +99,10 @@ public class PartitionRunnerTest { getPartitionRunner(5).runInPartitionedThreads(resourceIds, partitionConsumer); List calls = myLatch.awaitExpected(); PartitionCall partitionCall1 = (PartitionCall) PointcutLatch.getLatchInvocationParameter(calls, 0); - assertThat(partitionCall1.threadName, isOneOf(TEST_THREADNAME_1, TEST_THREADNAME_2)); + assertThat(partitionCall1.threadName, is(oneOf(TEST_THREADNAME_1, TEST_THREADNAME_2))); assertEquals(5, partitionCall1.size); PartitionCall partitionCall2 = (PartitionCall) PointcutLatch.getLatchInvocationParameter(calls, 1); - assertThat(partitionCall2.threadName, isOneOf(TEST_THREADNAME_1, TEST_THREADNAME_2)); + assertThat(partitionCall2.threadName, is(oneOf(TEST_THREADNAME_1, TEST_THREADNAME_2))); assertEquals(5, partitionCall2.size); assertNotEquals(partitionCall1.threadName, partitionCall2.threadName); } @@ -119,14 +120,38 @@ public class PartitionRunnerTest { getPartitionRunner(5).runInPartitionedThreads(resourceIds, partitionConsumer); List calls = myLatch.awaitExpected(); PartitionCall partitionCall1 = (PartitionCall) PointcutLatch.getLatchInvocationParameter(calls, 0); - assertThat(partitionCall1.threadName, isOneOf(TEST_THREADNAME_1, TEST_THREADNAME_2)); + assertThat(partitionCall1.threadName, is(oneOf(TEST_THREADNAME_1, TEST_THREADNAME_2))); assertEquals(true, nums.remove(partitionCall1.size)); PartitionCall partitionCall2 = (PartitionCall) PointcutLatch.getLatchInvocationParameter(calls, 1); - assertThat(partitionCall2.threadName, isOneOf(TEST_THREADNAME_1, TEST_THREADNAME_2)); + assertThat(partitionCall2.threadName, is(oneOf(TEST_THREADNAME_1, TEST_THREADNAME_2))); assertEquals(true, nums.remove(partitionCall2.size)); assertNotEquals(partitionCall1.threadName, partitionCall2.threadName); } + + + /** + * See #5636 $expunge operation ignoring ExpungeThreadCount setting in certain cases + */ + @Test + public void testExpunge_withTasksSizeBiggerThanExecutorQueue_usesConfiguredNumberOfThreads() throws InterruptedException { + // setup + List resourceIds = buildPidList(2500); + Consumer> partitionConsumer = buildPartitionConsumer(myLatch); + // with batch size = 2 we expect 2500/2 runnableTasks to be created + myLatch.setExpectedCount(1250); + + // execute + getPartitionRunner(2, 2).runInPartitionedThreads(resourceIds, partitionConsumer); + List calls = myLatch.awaitExpected(); + + // validate - only two threads should be used for execution + for (int i = 0; i < 1250; i++) { + PartitionCall partitionCall = (PartitionCall) PointcutLatch.getLatchInvocationParameter(calls, i); + assertThat(partitionCall.threadName, is(oneOf(TEST_THREADNAME_1, TEST_THREADNAME_2))); + } + } + @Test public void tenItemsOneThread() throws InterruptedException { List resourceIds = buildPidList(10); diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilderTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilderTest.java index 36766739cab..9da4f0b4cf2 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilderTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilderTest.java @@ -1,6 +1,14 @@ package ca.uhn.fhir.jpa.search.builder.predicate; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.svc.IIdHelperService; import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import com.healthmarketscience.sqlbuilder.BinaryCondition; import com.healthmarketscience.sqlbuilder.Condition; import com.healthmarketscience.sqlbuilder.InCondition; @@ -15,10 +23,14 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.when; @@ -26,11 +38,18 @@ import static org.mockito.Mockito.when; public class ResourceLinkPredicateBuilderTest { private static final String PLACEHOLDER_BASE = UUID.randomUUID().toString(); + private ResourceLinkPredicateBuilder myResourceLinkPredicateBuilder; @Mock private SearchQueryBuilder mySearchQueryBuilder; + @Mock + private ISearchParamRegistry mySearchParamRegistry; + + @Mock + private IIdHelperService myIdHelperService; + @BeforeEach public void init() { DbSpec spec = new DbSpec(); @@ -38,6 +57,8 @@ public class ResourceLinkPredicateBuilderTest { DbTable table = new DbTable(schema, "table"); when(mySearchQueryBuilder.addTable(Mockito.anyString())).thenReturn(table); myResourceLinkPredicateBuilder = new ResourceLinkPredicateBuilder(null, mySearchQueryBuilder, false); + myResourceLinkPredicateBuilder.setSearchParamRegistryForUnitTest(mySearchParamRegistry); + myResourceLinkPredicateBuilder.setIdHelperServiceForUnitTest(myIdHelperService); } @Test @@ -59,4 +80,25 @@ public class ResourceLinkPredicateBuilderTest { Condition condition = myResourceLinkPredicateBuilder.createEverythingPredicate("Patient", new ArrayList<>(), new Long[0]); assertEquals(BinaryCondition.class, condition.getClass()); } + + @Test + void validateInvalidModifiers() { + when(mySearchQueryBuilder.getFhirContext()).thenReturn(FhirContext.forR4Cached()); + final ReferenceParam referenceParam = new ReferenceParam(new IdDt(":identifier", "123")); + final List referenceOrParamList = List.of(referenceParam); + final SystemRequestDetails requestDetails = new SystemRequestDetails(); + final Map params = new LinkedHashMap<>(); + params.put("subject:identifier", new String[]{"1"}); + params.put("subject:x", new String[]{"2"}); + params.put("subject:y", new String[]{"3"}); + params.put("patient", new String[]{"4"}); + requestDetails.setParameters(params); + + try { + myResourceLinkPredicateBuilder.createPredicate(requestDetails, "Observation", "", Collections.emptyList(), referenceOrParamList, null, RequestPartitionId.allPartitions()); + fail(); + } catch (Exception exception) { + assertEquals("HAPI-2498: Unsupported search modifier(s): \"[:identifier, :x, :y]\" for resource type \"Observation\". Valid search modifiers are: [:contains, :exact, :in, :iterate, :missing, :not-in, :of-type, :recurse, :text]", exception.getMessage()); + } + } } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 86c244dfbf8..cf7b059cd15 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index ba0a5db50af..72f27397b49 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -2,15 +2,14 @@ package ca.uhn.fhirtest.config; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.r4.JpaR4Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc; import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl; +import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider; -import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; @@ -200,13 +199,12 @@ public class TestR4Config { @Bean public IIpsGenerationStrategy ipsGenerationStrategy() { - return new DefaultIpsGenerationStrategy(); + return new DefaultJpaIpsGenerationStrategy(); } @Bean - public IIpsGeneratorSvc ipsGeneratorSvc( - FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { - return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry); + public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) { + return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy); } @Bean diff --git a/hapi-fhir-server-cds-hooks/pom.xml b/hapi-fhir-server-cds-hooks/pom.xml index e54d84ed250..017633ee620 100644 --- a/hapi-fhir-server-cds-hooks/pom.xml +++ b/hapi-fhir-server-cds-hooks/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 9e56179c039..d269712f44d 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java index e3a91c24f32..4d8fef5e60f 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmSearchExpandingInterceptor.java @@ -72,7 +72,7 @@ public class MdmSearchExpandingInterceptor { theRequestDetails == null ? new SystemRequestDetails() : theRequestDetails; final RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( - requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap, null); + requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap); for (Map.Entry>> set : theSearchParameterMap.entrySet()) { String paramName = set.getKey(); List> andList = set.getValue(); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java index 396fdcaabb6..d903afd85cc 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java @@ -21,7 +21,6 @@ package ca.uhn.fhir.mdm.provider; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; @@ -143,13 +142,11 @@ public class MdmControllerHelper { public IBaseBundle getMatchesAndPossibleMatchesForResource( IAnyResource theResource, String theResourceType, RequestDetails theRequestDetails) { RequestPartitionId requestPartitionId; - ReadPartitionIdRequestDetails details = - ReadPartitionIdRequestDetails.forSearchType(theResourceType, null, null); if (myMdmSettings.getSearchAllPartitionForMatch()) { requestPartitionId = RequestPartitionId.allPartitions(); } else { - requestPartitionId = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details); + requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( + theRequestDetails, theResourceType); } List matches = myMdmMatchFinderSvc.getMatchedTargets(theResourceType, theResource, requestPartitionId); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSubmitSvcImpl.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSubmitSvcImpl.java index cb8f35c9149..d6bd5e99ea4 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSubmitSvcImpl.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSubmitSvcImpl.java @@ -106,7 +106,7 @@ public class MdmSubmitSvcImpl implements IMdmSubmitSvc { RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( - theRequestDetails, theSourceResourceType, spMap, null); + theRequestDetails, theSourceResourceType, spMap); return submitAllMatchingResourcesToMdmChannel(spMap, searchBuilder, requestPartitionId); } diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index 915d081dbc9..f55fb536f99 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 05911728343..e3e656cb7c5 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 534d9cd9dde..1efa0e7e751 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -25,12 +25,14 @@ import ca.uhn.fhir.i18n.Msg; 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.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor; +import ca.uhn.fhir.util.BundleUtil; import com.google.common.collect.Lists; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -78,8 +80,12 @@ public class AuthorizationInterceptor implements IRuleApplier { public static final String REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS = AuthorizationInterceptor.class.getName() + "_BulkDataExportOptions"; + public static final String BUNDLE = "Bundle"; private static final AtomicInteger ourInstanceCount = new AtomicInteger(0); private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class); + private static final Set STANDALONE_BUNDLE_RESOURCE_TYPES = + Set.of(BundleTypeEnum.DOCUMENT, BundleTypeEnum.COLLECTION, BundleTypeEnum.MESSAGE); + private final int myInstanceIndex = ourInstanceCount.incrementAndGet(); private final String myRequestSeenResourcesKey = AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES"; @@ -525,7 +531,7 @@ public class AuthorizationInterceptor implements IRuleApplier { case EXTENDED_OPERATION_TYPE: case EXTENDED_OPERATION_INSTANCE: { if (theResponseObject != null) { - resources = toListOfResourcesAndExcludeContainer(theResponseObject, fhirContext); + resources = toListOfResourcesAndExcludeContainer(theRequestDetails, theResponseObject, fhirContext); } break; } @@ -572,22 +578,23 @@ public class AuthorizationInterceptor implements IRuleApplier { OUT, } - static List toListOfResourcesAndExcludeContainer( - IBaseResource theResponseObject, FhirContext fhirContext) { + public static List toListOfResourcesAndExcludeContainer( + RequestDetails theRequestDetails, IBaseResource theResponseObject, FhirContext fhirContext) { if (theResponseObject == null) { return Collections.emptyList(); } List retVal; - boolean isContainer = false; + boolean shouldExamineChildResources = false; if (theResponseObject instanceof IBaseBundle) { - isContainer = true; + IBaseBundle bundle = (IBaseBundle) theResponseObject; + shouldExamineChildResources = shouldExamineBundleChildResources(theRequestDetails, fhirContext, bundle); } else if (theResponseObject instanceof IBaseParameters) { - isContainer = true; + shouldExamineChildResources = true; } - if (!isContainer) { + if (!shouldExamineChildResources) { return Collections.singletonList(theResponseObject); } @@ -604,6 +611,26 @@ public class AuthorizationInterceptor implements IRuleApplier { return retVal; } + /** + * This method determines if the given Bundle should have permissions applied to the resources inside or + * to the Bundle itself. + * + * This distinction is important in Bundle requests where a user has permissions to view all Bundles. In + * this scenario we want to apply permissions to the Bundle itself and not the resources inside if + * the Bundle is of type document, collection, or message. + */ + public static boolean shouldExamineBundleChildResources( + RequestDetails theRequestDetails, FhirContext theFhirContext, IBaseBundle theBundle) { + boolean isBundleRequest = theRequestDetails != null && BUNDLE.equals(theRequestDetails.getResourceName()); + if (!isBundleRequest) { + return true; + } + BundleTypeEnum bundleType = BundleUtil.getBundleTypeEnum(theFhirContext, theBundle); + boolean isStandaloneBundleResource = + bundleType != null && STANDALONE_BUNDLE_RESOURCE_TYPES.contains(bundleType); + return !isStandaloneBundleResource; + } + public static class Verdict { private final IAuthRule myDecidingRule; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index 460796e9121..f5c2dd1b141 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -63,6 +63,8 @@ import static org.hl7.fhir.instance.model.api.IAnyResource.SP_RES_ID; @SuppressWarnings("EnumSwitchStatementWhichMissesCases") class RuleImplOp extends BaseRule /* implements IAuthRule */ { private static final Logger ourLog = LoggerFactory.getLogger(RuleImplOp.class); + private static final String PARAMETERS = "Parameters"; + private static final String BUNDLE = "Bundle"; private AppliesTypeEnum myAppliesTo; private Set myAppliesToTypes; @@ -771,7 +773,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { */ if (nextPart.getResource() != null) { RuntimeResourceDefinition resourceDef = ctx.getResourceDefinition(nextPart.getResource()); - if ("Parameters".equals(resourceDef.getName()) || "Bundle".equals(resourceDef.getName())) { + + // TODO: LD: We should pursue a more ideal fix after the release to inspect the bundle more deeply + // to ensure that it's a valid request + if (shouldRejectBundleEntry(resourceDef, operation)) { throw new InvalidRequestException(Msg.code(339) + "Can not handle transaction with nested resource of type " + resourceDef.getName()); } @@ -812,7 +817,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } else if (theOutputResource != null) { List outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer( - theOutputResource, theRequestDetails.getFhirContext()); + theRequestDetails, theOutputResource, theRequestDetails.getFhirContext()); Verdict verdict = null; for (IBaseResource nextResource : outputResources) { @@ -835,6 +840,24 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } } + /** + * Ascertain whether this transaction request contains a nested operations or nested transactions. + * This is done carefully because a bundle can contain a nested PATCH with Parameters, which is supported but + * a non-PATCH nested Parameters resource may be problematic. + * + * @param theResourceDef The {@link RuntimeResourceDefinition} associated with this bundle entry + * @param theOperation The {@link RestOperationTypeEnum} associated with this bundle entry + * @return true if we should reject this reject + */ + private boolean shouldRejectBundleEntry( + RuntimeResourceDefinition theResourceDef, RestOperationTypeEnum theOperation) { + final boolean isResourceParameters = PARAMETERS.equals(theResourceDef.getName()); + final boolean isResourceBundle = BUNDLE.equals(theResourceDef.getName()); + final boolean isOperationPatch = theOperation == RestOperationTypeEnum.PATCH; + + return (isResourceParameters && !isOperationPatch) || isResourceBundle; + } + private void setTargetFromResourceId(RequestDetails theRequestDetails, FhirContext ctx, RuleTarget target) { String[] idValues = theRequestDetails.getParameters().get(SP_RES_ID); target.resourceIds = new ArrayList<>(); @@ -909,7 +932,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { private boolean requestAppliesToTransaction( FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) { - if (!"Bundle".equals(theContext.getResourceType(theInputResource))) { + if (!BUNDLE.equals(theContext.getResourceType(theInputResource))) { return false; } diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index 7d29c911aa4..a506b2ce079 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml index c0869cd481d..8f0c169d396 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml @@ -21,7 +21,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/src/main/java/ca/uhn/fhir/sl/cache/caffeine/CacheProvider.java b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/src/main/java/ca/uhn/fhir/sl/cache/caffeine/CacheProvider.java index 6a1376410f0..cc324964f02 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/src/main/java/ca/uhn/fhir/sl/cache/caffeine/CacheProvider.java +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/src/main/java/ca/uhn/fhir/sl/cache/caffeine/CacheProvider.java @@ -44,6 +44,9 @@ public class CacheProvider implements ca.uhn.fhir.sl.cache.CacheProvider create(long timeoutMillis, long maximumSize) { return new CacheDelegator(Caffeine.newBuilder() .expireAfterWrite(timeoutMillis, TimeUnit.MILLISECONDS) + // Caffeine locks the whole array when growing the hash table. + // Set initial capacity to max to avoid this. All our caches are <1M entries. + .initialCapacity((int) maximumSize) .maximumSize(maximumSize) .build()); } @@ -51,6 +54,9 @@ public class CacheProvider implements ca.uhn.fhir.sl.cache.CacheProvider create(long timeoutMillis, long maximumSize, CacheLoader loading) { return new LoadingCacheDelegator(Caffeine.newBuilder() .expireAfterWrite(timeoutMillis, TimeUnit.MILLISECONDS) + // Caffeine locks the whole array when growing the hash table. + // Set initial capacity to max to avoid this. All our caches are <1M entries. + .initialCapacity((int) maximumSize) .maximumSize(maximumSize) .build(loading::load)); } diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index ea9a1742b73..82e6cc77ea2 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml index b721c4adccc..949ff5ecae2 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml @@ -7,7 +7,7 @@ hapi-fhir ca.uhn.hapi.fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index 0d1b280f250..7eebe9b96b9 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 4235822f44e..356902173cf 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index cfbea296732..445db4a2a14 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index dc858d2dd45..d96dadd16bf 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index b7076dd414e..572e2290c65 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index 66adce27094..7750239178c 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index c13b482b291..5005ad6e451 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 33b7eaeecbd..0359034b9ac 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index 2e4860c5057..dd31246c492 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java index 3f47dc4606d..387cea1aeb7 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java @@ -597,11 +597,11 @@ public class Builder { "Only SELECT statements (including CTEs) are allowed here. Please check your SQL: [%s]", theSql)); } - ourLog.info("SQL to evaluate: {}", theSql); + ourLog.debug("SQL to evaluate: {}", theSql); myTask.addPrecondition(new ExecuteTaskPrecondition( () -> { - ourLog.info("Checking precondition for SQL: {}", theSql); + ourLog.debug("Checking precondition for SQL: {}", theSql); return MigrationJdbcUtils.queryForSingleBooleanResultMultipleThrowsException( theSql, myTask.newJdbcTemplate()); }, @@ -614,6 +614,11 @@ public class Builder { myTask.setRunDuringSchemaInitialization(true); return this; } + + public BuilderCompleteTask setTransactional(boolean theFlag) { + myTask.setTransactional(theFlag); + return this; + } } public class BuilderAddTableRawSql { diff --git a/hapi-fhir-storage-batch2-jobs/pom.xml b/hapi-fhir-storage-batch2-jobs/pom.xml index 14cc4359d72..af690eb9694 100644 --- a/hapi-fhir-storage-batch2-jobs/pom.xml +++ b/hapi-fhir-storage-batch2-jobs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java index 68629750740..8882abc5c2e 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java @@ -187,7 +187,8 @@ public class BulkDataExportProvider { // Determine and validate partition permissions (if needed). RequestPartitionId partitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, null); + myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.OPERATION_EXPORT); myRequestPartitionHelperService.validateHasPartitionPermissions(theRequestDetails, "Binary", partitionId); theOptions.setPartitionId(partitionId); @@ -468,7 +469,8 @@ public class BulkDataExportProvider { if (parameters.getPartitionId() != null) { // Determine and validate permissions for partition (if needed) RequestPartitionId partitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, null); + myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.OPERATION_EXPORT_POLL_STATUS); myRequestPartitionHelperService.validateHasPartitionPermissions(theRequestDetails, "Binary", partitionId); if (!parameters.getPartitionId().equals(partitionId)) { throw new InvalidRequestException( @@ -502,6 +504,9 @@ public class BulkDataExportProvider { String serverBase = getServerBase(theRequestDetails); + // an output is required, even if empty, according to HL7 FHIR IG + bulkResponseDocument.getOutput(); + for (Map.Entry> entrySet : results.getResourceTypeToBinaryIds().entrySet()) { String resourceType = entrySet.getKey(); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java index f30c96d59f3..5fe7b69687d 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.i18n.Msg; 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.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; @@ -103,11 +102,10 @@ public class DeleteExpungeJobSubmitterImpl implements IDeleteExpungeJobSubmitter .forEach(deleteExpungeJobParameters::addPartitionedUrl); deleteExpungeJobParameters.setBatchSize(theBatchSize); - ReadPartitionIdRequestDetails details = - ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_DELETE_EXPUNGE); - // Also set toplevel partition in case there are no urls + // Also set top level partition in case there are no urls RequestPartitionId requestPartition = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details); + myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.OPERATION_DELETE_EXPUNGE); deleteExpungeJobParameters.setRequestPartitionId(requestPartition); deleteExpungeJobParameters.setCascade(theCascade); deleteExpungeJobParameters.setCascadeMaxRounds(theCascadeMaxRounds); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProvider.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProvider.java index 35d5b3d9ca4..ca1b4b3032f 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProvider.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProvider.java @@ -157,8 +157,9 @@ public class BulkDataImportProvider { } RequestPartitionId partitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, null); - if (partitionId != null && !partitionId.isAllPartitions()) { + myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( + theRequestDetails, JpaConstants.OPERATION_IMPORT); + if (!partitionId.isAllPartitions()) { myRequestPartitionHelperService.validateHasPartitionPermissions(theRequestDetails, "Binary", partitionId); jobParameters.setPartitionId(partitionId); } @@ -234,7 +235,8 @@ public class BulkDataImportProvider { if (parameters != null && parameters.getPartitionId() != null) { // Determine and validate permissions for partition (if needed) RequestPartitionId partitionId = - myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, null); + myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( + theRequestDetails, JpaConstants.OPERATION_IMPORT); myRequestPartitionHelperService.validateHasPartitionPermissions(theRequestDetails, "Binary", partitionId); if (!partitionId.equals(parameters.getPartitionId())) { throw new InvalidRequestException( diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportFileServlet.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportFileServlet.java index 0943e5b2d31..c2ab1cfee05 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportFileServlet.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportFileServlet.java @@ -38,6 +38,7 @@ import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -56,8 +57,36 @@ public class BulkImportFileServlet extends HttpServlet { public static final String DEFAULT_HEADER_CONTENT_TYPE = CT_FHIR_NDJSON + CHARSET_UTF8_CTSUFFIX; + private String myBasicAuth; + + public BulkImportFileServlet() {} + + public BulkImportFileServlet(String theBasicAuthUsername, String theBasicAuthPassword) { + setBasicAuth(theBasicAuthUsername, theBasicAuthPassword); + } + + public void setBasicAuth(String username, String password) { + String auth = username + ":" + password; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes()); + myBasicAuth = "Basic " + encodedAuth; + } + + public void checkBasicAuthAndMaybeThrow403(HttpServletRequest request, HttpServletResponse response) + throws IOException { + // Check if the myBasicAuth variable is set, ignore if not. + if (myBasicAuth == null || myBasicAuth.isEmpty()) { + return; + } + + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.equals(myBasicAuth)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid authentication credentials."); + } + } + @Override protected void doGet(HttpServletRequest theRequest, HttpServletResponse theResponse) throws IOException { + checkBasicAuthAndMaybeThrow403(theRequest, theResponse); try { String servletPath = theRequest.getServletPath(); String requestUri = theRequest.getRequestURI(); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportJobParameters.java index 5ede8fd0438..9617db73a0f 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/imprt/BulkImportJobParameters.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.batch2.jobs.imprt; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.model.api.IModelJson; +import ca.uhn.fhir.model.api.annotation.SensitiveNoDisplay; +import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.annotation.Nullable; import jakarta.validation.constraints.Min; @@ -36,6 +38,8 @@ import java.util.List; * This class is the parameters model object for starting a * bulk import job. */ +@JsonFilter(IModelJson.SENSITIVE_DATA_FILTER_NAME) // TODO GGG eventually consider pushing this up once we have more +// experience using it. public class BulkImportJobParameters implements IModelJson { @JsonProperty(value = "ndJsonUrls", required = true) @@ -43,8 +47,9 @@ public class BulkImportJobParameters implements IModelJson { @NotNull(message = "At least one NDJSON URL must be provided") private List<@Pattern(regexp = "^http[s]?://.*", message = "Must be a valid URL") String> myNdJsonUrls; - @JsonProperty(value = "httpBasicCredentials", access = JsonProperty.Access.WRITE_ONLY, required = false) + @JsonProperty(value = "httpBasicCredentials", required = false) @Nullable + @SensitiveNoDisplay private String myHttpBasicCredentials; @JsonProperty(value = "maxBatchResourceCount", required = false) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProvider.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProvider.java index a57642312b4..1c843474a35 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProvider.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProvider.java @@ -23,7 +23,6 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.parameters.UrlPartitioner; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.ReindexParameters; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; @@ -129,10 +128,9 @@ public class ReindexProvider { .forEach(params::addPartitionedUrl); } - ReadPartitionIdRequestDetails details = - ReadPartitionIdRequestDetails.forOperation(null, null, ProviderConstants.OPERATION_REINDEX); RequestPartitionId requestPartition = - myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details); + myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( + theRequestDetails, ProviderConstants.OPERATION_REINDEX); params.setRequestPartitionId(requestPartition); JobInstanceStartRequest request = new JobInstanceStartRequest(); diff --git a/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProviderTest.java b/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProviderTest.java index 4d79239db44..ddc1fde19f5 100644 --- a/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProviderTest.java +++ b/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/BulkDataImportProviderTest.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; import ca.uhn.fhir.test.utilities.HttpClientExtension; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import com.google.common.base.Charsets; +import jakarta.annotation.Nonnull; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -50,7 +51,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.annotation.Nonnull; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Date; @@ -154,7 +154,7 @@ public class BulkDataImportProviderTest { JobInstanceStartRequest startRequest = myStartRequestCaptor.getValue(); ourLog.info("Parameters: {}", startRequest.getParameters()); - assertTrue(startRequest.getParameters().startsWith("{\"ndJsonUrls\":[\"http://example.com/Patient\",\"http://example.com/Observation\"],\"maxBatchResourceCount\":500")); + assertTrue(startRequest.getParameters().startsWith("{\"ndJsonUrls\":[\"http://example.com/Patient\",\"http://example.com/Observation\"],\"httpBasicCredentials\":\"admin:password\",\"maxBatchResourceCount\":500,\"partitionId\":{\"allPartitions\":false")); } @Test @@ -399,7 +399,7 @@ public class BulkDataImportProviderTest { private class MyRequestPartitionHelperSvc implements IRequestPartitionHelperSvc { @Nonnull @Override - public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, ReadPartitionIdRequestDetails theDetails) { + public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, @Nonnull ReadPartitionIdRequestDetails theDetails) { assert theRequest != null; if (myPartitionName.equals(theRequest.getTenantId())) { return myRequestPartitionId; @@ -409,7 +409,7 @@ public class BulkDataImportProviderTest { } @Override - public void validateHasPartitionPermissions(RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { + public void validateHasPartitionPermissions(@Nonnull RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { if (!myPartitionName.equals(theRequest.getTenantId()) && theRequest.getTenantId() != null) { throw new ForbiddenOperationException("User does not have access to resources on the requested partition"); } diff --git a/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/ParameterSerializationTest.java b/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/ParameterSerializationTest.java new file mode 100644 index 00000000000..6ae435532bc --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/imprt/ParameterSerializationTest.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.batch2.jobs.imprt; + +import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.util.JsonUtil; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +public class ParameterSerializationTest { + + @Test + public void testBatchJobParametersSuccessfullySerializeAllFields() { + JobInstanceStartRequest startRequest = new JobInstanceStartRequest(); + BulkImportJobParameters parameters = new BulkImportJobParameters(); + parameters.addNdJsonUrl("myurl"); + parameters.setHttpBasicCredentials("username:password"); + startRequest.setParameters(parameters); + + BulkImportJobParameters readBackParameters = startRequest.getParameters(BulkImportJobParameters.class); + + assertThat(readBackParameters.getHttpBasicCredentials(), is(equalTo("username:password"))); + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProviderTest.java b/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProviderTest.java index f2559688dbe..b0ee77b1379 100644 --- a/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProviderTest.java +++ b/hapi-fhir-storage-batch2-jobs/src/test/java/ca/uhn/fhir/batch2/jobs/reindex/ReindexProviderTest.java @@ -74,7 +74,7 @@ public class ReindexProviderTest { when(myJobCoordinator.startInstance(isNotNull(), any())) .thenReturn(createJobStartResponse()); - when(myRequestPartitionHelperSvc.determineReadPartitionForRequest(any(), any())).thenReturn(RequestPartitionId.allPartitions()); + when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation(any(), any())).thenReturn(RequestPartitionId.allPartitions()); } private Batch2JobStartResponse createJobStartResponse() { diff --git a/hapi-fhir-storage-batch2-test-utilities/pom.xml b/hapi-fhir-storage-batch2-test-utilities/pom.xml index 16743858218..2955d87ddc5 100644 --- a/hapi-fhir-storage-batch2-test-utilities/pom.xml +++ b/hapi-fhir-storage-batch2-test-utilities/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index 2afd5c63f89..bda82a295a2 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java index b0ae258fd27..a39dccb9a75 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java @@ -75,9 +75,10 @@ public class JobStepExecutor { instance.setEndTime(new Date()); diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/UrlPartitioner.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/UrlPartitioner.java index 040a379e025..0d5c2e75b84 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/UrlPartitioner.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/UrlPartitioner.java @@ -38,10 +38,7 @@ public class UrlPartitioner { ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl); RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( - theRequestDetails, - resourceSearch.getResourceName(), - resourceSearch.getSearchParameterMap(), - null); + theRequestDetails, resourceSearch.getResourceName(), resourceSearch.getSearchParameterMap()); PartitionedUrl retval = new PartitionedUrl(); retval.setUrl(theUrl); retval.setRequestPartitionId(requestPartitionId); diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java index 29ac2faf4f5..d10ca861a3f 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java @@ -145,6 +145,11 @@ public class JobDefinition { return myGatedExecution; } + public boolean isLastStepReduction() { + int stepCount = getSteps().size(); + return stepCount >= 1 && getSteps().get(stepCount - 1).isReductionStep(); + } + public int getStepIndex(String theStepId) { int retVal = myStepIds.indexOf(theStepId); Validate.isTrue(retVal != -1); @@ -304,9 +309,9 @@ public class JobDefinition { throw new ConfigurationException(Msg.code(2106) + String.format("Job Definition %s has a reducer step but is not gated", myJobDefinitionId)); } - mySteps.add(new JobDefinitionReductionStep( + mySteps.add(new JobDefinitionReductionStep<>( theStepId, theStepDescription, theStepWorker, myNextInputType, theOutputType)); - return new Builder( + return new Builder<>( mySteps, myJobDefinitionId, myJobDefinitionVersion, diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobInstanceStartRequest.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobInstanceStartRequest.java index d32b4209058..607ebb5ec55 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobInstanceStartRequest.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobInstanceStartRequest.java @@ -83,7 +83,7 @@ public class JobInstanceStartRequest implements IModelJson { } public JobInstanceStartRequest setParameters(IModelJson theParameters) { - myParameters = JsonUtil.serializeOrInvalidRequest(theParameters); + myParameters = JsonUtil.serializeWithSensitiveData(theParameters); return this; } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java index a21d6c595e2..790ed970c1a 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java @@ -192,12 +192,12 @@ public class InstanceProgress { /** * Transitions from IN_PROGRESS/ERRORED based on chunk statuses. */ - public void calculateNewStatus() { + public void calculateNewStatus(boolean theLastStepIsReduction) { if (myFailedChunkCount > 0) { myNewStatus = StatusEnum.FAILED; } else if (myErroredChunkCount > 0) { myNewStatus = StatusEnum.ERRORED; - } else if (myIncompleteChunkCount == 0 && myCompleteChunkCount > 0) { + } else if (myIncompleteChunkCount == 0 && myCompleteChunkCount > 0 && !theLastStepIsReduction) { myNewStatus = StatusEnum.COMPLETED; } } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java index e5ce87c8a58..348fd30e540 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java @@ -22,19 +22,26 @@ package ca.uhn.fhir.batch2.progress; import ca.uhn.fhir.batch2.api.IJobPersistence; import ca.uhn.fhir.batch2.coordinator.JobDefinitionRegistry; import ca.uhn.fhir.batch2.maintenance.JobChunkProgressAccumulator; +import ca.uhn.fhir.batch2.model.JobDefinition; +import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.WorkChunk; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.model.api.IModelJson; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.util.StopWatch; import jakarta.annotation.Nonnull; import org.slf4j.Logger; import java.util.Iterator; +import java.util.Optional; public class JobInstanceProgressCalculator { private static final Logger ourLog = Logs.getBatchTroubleshootingLog(); private final IJobPersistence myJobPersistence; private final JobChunkProgressAccumulator myProgressAccumulator; private final JobInstanceStatusUpdater myJobInstanceStatusUpdater; + private final JobDefinitionRegistry myJobDefinitionRegistry; public JobInstanceProgressCalculator( IJobPersistence theJobPersistence, @@ -42,6 +49,7 @@ public class JobInstanceProgressCalculator { JobDefinitionRegistry theJobDefinitionRegistry) { myJobPersistence = theJobPersistence; myProgressAccumulator = theProgressAccumulator; + myJobDefinitionRegistry = theJobDefinitionRegistry; myJobInstanceStatusUpdater = new JobInstanceStatusUpdater(theJobDefinitionRegistry); } @@ -96,8 +104,20 @@ public class JobInstanceProgressCalculator { } // wipmb separate status update from stats collection in 6.8 - instanceProgress.calculateNewStatus(); + instanceProgress.calculateNewStatus(lastStepIsReduction(instanceId)); return instanceProgress; } + + private boolean lastStepIsReduction(String theInstanceId) { + JobInstance jobInstance = getJobInstance(theInstanceId); + JobDefinition jobDefinition = myJobDefinitionRegistry.getJobDefinitionOrThrowException(jobInstance); + return jobDefinition.isLastStepReduction(); + } + + private JobInstance getJobInstance(String theInstanceId) { + Optional oInstance = myJobPersistence.fetchInstance(theInstanceId); + return oInstance.orElseThrow(() -> + new InternalErrorException(Msg.code(2486) + "Failed to fetch JobInstance with id: " + theInstanceId)); + } } diff --git a/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java b/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java index 425f1d4b66e..28d246ccd4f 100644 --- a/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java +++ b/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java @@ -22,6 +22,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Collections; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -30,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -90,12 +92,16 @@ public class ReductionStepDataSinkTest { String data = "data"; StepOutputData stepData = new StepOutputData(data); WorkChunkData chunkData = new WorkChunkData<>(stepData); + @SuppressWarnings("unchecked") + JobDefinition jobDefinition = mock(JobDefinition.class); // when JobInstance instance = JobInstance.fromInstanceId(INSTANCE_ID); instance.setStatus(StatusEnum.FINALIZE); stubUpdateInstanceCallback(instance); when(myJobPersistence.fetchAllWorkChunksIterator(any(), anyBoolean())).thenReturn(Collections.emptyIterator()); + when(myJobPersistence.fetchInstance(INSTANCE_ID)).thenReturn(Optional.of(instance)); + when(myJobDefinitionRegistry.getJobDefinitionOrThrowException(instance)).thenReturn(jobDefinition); // test myDataSink.accept(chunkData); @@ -111,6 +117,8 @@ public class ReductionStepDataSinkTest { String data2 = "data2"; WorkChunkData firstData = new WorkChunkData<>(new StepOutputData(data)); WorkChunkData secondData = new WorkChunkData<>(new StepOutputData(data2)); + @SuppressWarnings("unchecked") + JobDefinition jobDefinition = mock(JobDefinition.class); ourLogger.setLevel(Level.ERROR); @@ -118,6 +126,8 @@ public class ReductionStepDataSinkTest { instance.setStatus(StatusEnum.FINALIZE); when(myJobPersistence.fetchAllWorkChunksIterator(any(), anyBoolean())).thenReturn(Collections.emptyIterator()); stubUpdateInstanceCallback(instance); + when(myJobPersistence.fetchInstance(INSTANCE_ID)).thenReturn(Optional.of(instance)); + when(myJobDefinitionRegistry.getJobDefinitionOrThrowException(instance)).thenReturn(jobDefinition); // test myDataSink.accept(firstData); @@ -136,10 +146,15 @@ public class ReductionStepDataSinkTest { @Test public void accept_noInstanceIdFound_throwsJobExecutionFailed() { // setup + JobInstance jobInstance = mock(JobInstance.class); + @SuppressWarnings("unchecked") + JobDefinition jobDefinition = (JobDefinition) mock(JobDefinition.class); String data = "data"; WorkChunkData chunkData = new WorkChunkData<>(new StepOutputData(data)); when(myJobPersistence.updateInstance(any(), any())).thenReturn(false); when(myJobPersistence.fetchAllWorkChunksIterator(any(), anyBoolean())).thenReturn(Collections.emptyIterator()); + when(myJobPersistence.fetchInstance(INSTANCE_ID)).thenReturn(Optional.of(jobInstance)); + when(myJobDefinitionRegistry.getJobDefinitionOrThrowException(jobInstance)).thenReturn(jobDefinition); // test try { @@ -151,5 +166,4 @@ public class ReductionStepDataSinkTest { fail("Unexpected exception", anyOtherEx); } } - } diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index 6c432220dbb..191bc8145e3 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index a542f34b632..f12bb7bf47e 100644 --- a/hapi-fhir-storage-mdm/pom.xml +++ b/hapi-fhir-storage-mdm/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index c83f1186526..e4a2f25b000 100644 --- a/hapi-fhir-storage-test-utilities/pom.xml +++ b/hapi-fhir-storage-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-test-utilities/src/test/java/ca/uhn/fhir/storage/test/BaseTransactionProcessorTest.java b/hapi-fhir-storage-test-utilities/src/test/java/ca/uhn/fhir/storage/test/BaseTransactionProcessorTest.java index f8e70e6b624..d19088baaf9 100644 --- a/hapi-fhir-storage-test-utilities/src/test/java/ca/uhn/fhir/storage/test/BaseTransactionProcessorTest.java +++ b/hapi-fhir-storage-test-utilities/src/test/java/ca/uhn/fhir/storage/test/BaseTransactionProcessorTest.java @@ -110,4 +110,21 @@ public class BaseTransactionProcessorTest { assertTrue(matchResult, "Failed to find a Regex match using Url '" + matchUrl + "'"); } + @Test + void identifierSubstitutionNoQuestionMark() { + final IdSubstitutionMap idSubstitutions = new IdSubstitutionMap(); + idSubstitutions.put(new IdType("Task/urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"), new IdType("Task/1/history/1")); + idSubstitutions.put(new IdType("urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"), new IdType("Task/1/_history/1")); + final String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, "identifier=http://tempuri.org|2&based-on=urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"); + assertEquals("identifier=http://tempuri.org|2&based-on=Task/1", outcome); + } + + @Test + void identifierSubstitutionYesQuestionMar() { + final IdSubstitutionMap idSubstitutions = new IdSubstitutionMap(); + idSubstitutions.put(new IdType("Task/urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"), new IdType("Task/1/history/1")); + idSubstitutions.put(new IdType("urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"), new IdType("Task/1/_history/1")); + final String outcome = BaseTransactionProcessor.performIdSubstitutionsInMatchUrl(idSubstitutions, "?identifier=http://tempuri.org|2&based-on=urn:uuid:59cda086-4763-4ef0-8e36-8c90058686ea"); + assertEquals("?identifier=http://tempuri.org|2&based-on=Task/1", outcome); + } } diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index fa2c439f96a..c289192e834 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 50d5231c7f6..2c86b83a33c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -110,6 +110,7 @@ public class JpaStorageSettings extends StorageSettings { private static final Integer DEFAULT_INTERNAL_SYNCHRONOUS_SEARCH_SIZE = 10000; private static final boolean DEFAULT_PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA = false; + private static final long DEFAULT_REST_DELETE_BY_URL_RESOURCE_ID_THRESHOLD = 10000; /** * Do not change default of {@code 0}! @@ -344,6 +345,13 @@ public class JpaStorageSettings extends StorageSettings { private boolean myPreventInvalidatingConditionalMatchCriteria = DEFAULT_PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA; + /** + * This setting helps to enforce a threshold in number of resolved resources for DELETE by URL REST calls + * + * @since 7.2.0 + */ + private long myRestDeleteByUrlResourceIdThreshold = DEFAULT_REST_DELETE_BY_URL_RESOURCE_ID_THRESHOLD; + /** * Constructor */ @@ -2427,6 +2435,14 @@ public class JpaStorageSettings extends StorageSettings { return myPreventInvalidatingConditionalMatchCriteria; } + public long getRestDeleteByUrlResourceIdThreshold() { + return myRestDeleteByUrlResourceIdThreshold; + } + + public void setRestDeleteByUrlResourceIdThreshold(long theRestDeleteByUrlResourceIdThreshold) { + myRestDeleteByUrlResourceIdThreshold = theRestDeleteByUrlResourceIdThreshold; + } + public enum StoreMetaSourceInformationEnum { NONE(false, false), SOURCE_URI(true, false), diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index 08cb4b8b03f..c8a18f9c7e3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -1745,16 +1745,18 @@ public abstract class BaseTransactionProcessor { continue; // No substitution on the resource ID itself! } String nextUriString = nextRef.getValueAsString(); - if (theIdSubstitutions.containsSource(nextUriString)) { - IIdType newId = theIdSubstitutions.getForSource(nextUriString); - ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); + if (isNotBlank(nextUriString)) { + if (theIdSubstitutions.containsSource(nextUriString)) { + IIdType newId = theIdSubstitutions.getForSource(nextUriString); + ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); - String existingValue = nextRef.getValueAsString(); - theTransactionDetails.addRollbackUndoAction(() -> nextRef.setValueAsString(existingValue)); + String existingValue = nextRef.getValueAsString(); + theTransactionDetails.addRollbackUndoAction(() -> nextRef.setValueAsString(existingValue)); - nextRef.setValueAsString(newId.toVersionless().getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); + nextRef.setValueAsString(newId.toVersionless().getValue()); + } else { + ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); + } } } @@ -1806,10 +1808,7 @@ public abstract class BaseTransactionProcessor { theDaoMethodOutcome.setId(newId); - IIdType target = theIdSubstitutions.getForSource(newId); - if (target != null) { - target.setValue(newId.getValue()); - } + theIdSubstitutions.updateTargets(newId); if (theDaoMethodOutcome.getOperationOutcome() != null) { IBase responseEntry = entriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId); @@ -2256,8 +2255,7 @@ public abstract class BaseTransactionProcessor { public static String performIdSubstitutionsInMatchUrl(IdSubstitutionMap theIdSubstitutions, String theMatchUrl) { String matchUrl = theMatchUrl; if (isNotBlank(matchUrl) && !theIdSubstitutions.isEmpty()) { - - int startIdx = matchUrl.indexOf('?'); + int startIdx = 0; while (startIdx != -1) { int endIdx = matchUrl.indexOf('&', startIdx + 1); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMap.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMap.java index 26130af1b2d..66a17baedef 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMap.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMap.java @@ -27,6 +27,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; public class IdSubstitutionMap { @@ -87,6 +88,22 @@ public class IdSubstitutionMap { return myMap.isEmpty(); } + /** + * Updates all targets of the map with a new id value if the input id has + * the same ResourceType and IdPart as the target id. + */ + public void updateTargets(IIdType theNewId) { + if (theNewId == null) { + return; + } + String newUnqualifiedVersionLessId = theNewId.toUnqualifiedVersionless().getValue(); + entrySet().stream() + .map(Pair::getValue) + .filter(targetId -> + Objects.equals(targetId.toUnqualifiedVersionless().getValue(), newUnqualifiedVersionLessId)) + .forEach(targetId -> targetId.setValue(theNewId.getValue())); + } + private static class Entry { private final String myUnversionedId; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunner.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunner.java index 4f057ac8c97..98e2d5cceb4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunner.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/expunge/PartitionRunner.java @@ -184,9 +184,13 @@ public class PartitionRunner { } ourLog.info("Slot become available after {}ms", sw.getMillis()); }; + + // setting corePoolSize and maximumPoolSize to be the same as threadCount + // to ensure that the number of allocated threads for the expunge operation does not exceed the configured limit + // see ThreadPoolExecutor documentation for details return new ThreadPoolExecutor( threadCount, - MAX_POOL_SIZE, + threadCount, 0L, TimeUnit.MILLISECONDS, executorQueue, diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java index 247eb8d0232..ac592d245e3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java @@ -247,6 +247,7 @@ public class HapiTransactionService implements IHapiTransactionService { ourRequestPartitionThreadLocal.set(requestPartitionId); } + ourLog.trace("Starting doExecute for RequestPartitionId {}", requestPartitionId); if (!myPartitionSettings.isPartitioningEnabled() || Objects.equals(previousRequestPartitionId, requestPartitionId)) { if (ourExistingTransaction.get() == this && canReuseExistingTransaction(theExecutionBuilder)) { @@ -281,6 +282,7 @@ public class HapiTransactionService implements IHapiTransactionService { TransactionCallback theCallback, RequestPartitionId requestPartitionId, RequestPartitionId previousRequestPartitionId) { + ourLog.trace("executeInNewTransactionForPartitionChange"); theExecutionBuilder.myPropagation = myTransactionPropagationWhenChangingPartitions; return doExecuteInTransaction(theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId); } @@ -310,6 +312,7 @@ public class HapiTransactionService implements IHapiTransactionService { TransactionCallback theCallback, RequestPartitionId requestPartitionId, RequestPartitionId previousRequestPartitionId) { + ourLog.trace("doExecuteInTransaction"); try { for (int i = 0; ; i++) { try { @@ -569,6 +572,7 @@ public class HapiTransactionService implements IHapiTransactionService { @Nullable private static T executeInExistingTransaction(@Nonnull TransactionCallback theCallback) { + ourLog.trace("executeInExistingTransaction"); // TODO we could probably track the TransactionStatus we need as a thread local like we do our partition id. return theCallback.doInTransaction(null); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/DaoRegistryGraphQLStorageServices.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/DaoRegistryGraphQLStorageServices.java index 94efc291f62..8eeec832887 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/DaoRegistryGraphQLStorageServices.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/graphql/DaoRegistryGraphQLStorageServices.java @@ -320,8 +320,8 @@ public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageService CacheControlDirective cacheControlDirective = new CacheControlDirective(); cacheControlDirective.parse(requestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)); - RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType( - requestDetails, theType, params, null); + RequestPartitionId requestPartitionId = + myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(requestDetails, theType, params); response = mySearchCoordinatorSvc.registerSearch( getDao(theType), params, theType, cacheControlDirective, requestDetails, requestPartitionId); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptor.java index f399b19a383..c563f19e9d9 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/interceptor/PatientIdPartitionInterceptor.java @@ -33,7 +33,6 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; import ca.uhn.fhir.jpa.util.ResourceCompartmentUtil; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; @@ -43,11 +42,13 @@ import org.hl7.fhir.r4.model.IdType; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** @@ -67,13 +68,6 @@ public class PatientIdPartitionInterceptor { @Autowired private PartitionSettings myPartitionSettings; - /** - * Constructor - */ - public PatientIdPartitionInterceptor() { - super(); - } - /** * Constructor */ @@ -81,7 +75,6 @@ public class PatientIdPartitionInterceptor { FhirContext theFhirContext, ISearchParamExtractor theSearchParamExtractor, PartitionSettings thePartitionSettings) { - this(); myFhirContext = theFhirContext; mySearchParamExtractor = theSearchParamExtractor; myPartitionSettings = thePartitionSettings; @@ -116,15 +109,15 @@ public class PatientIdPartitionInterceptor { @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ) public RequestPartitionId identifyForRead( - ReadPartitionIdRequestDetails theReadDetails, RequestDetails theRequestDetails) { - if (isBlank(theReadDetails.getResourceType())) { - return provideNonCompartmentMemberTypeResponse(null); - } - RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theReadDetails.getResourceType()); - List compartmentSps = - ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef); - if (compartmentSps.isEmpty()) { - return provideNonCompartmentMemberTypeResponse(null); + @Nonnull ReadPartitionIdRequestDetails theReadDetails, RequestDetails theRequestDetails) { + List compartmentSps = Collections.emptyList(); + if (!isEmpty(theReadDetails.getResourceType())) { + RuntimeResourceDefinition resourceDef = + myFhirContext.getResourceDefinition(theReadDetails.getResourceType()); + compartmentSps = ResourceCompartmentUtil.getPatientCompartmentSearchParams(resourceDef); + if (compartmentSps.isEmpty()) { + return provideNonCompartmentMemberTypeResponse(null); + } } //noinspection EnumSwitchStatementWhichMissesCases @@ -139,10 +132,9 @@ public class PatientIdPartitionInterceptor { break; case SEARCH_TYPE: SearchParameterMap params = theReadDetails.getSearchParams(); - + assert params != null; if ("Patient".equals(theReadDetails.getResourceType())) { List idParts = getResourceIdList(params, "_id", "Patient", false); - if (idParts.size() == 1) { return provideCompartmentMemberInstanceResponse(theRequestDetails, idParts.get(0)); } else { @@ -158,11 +150,16 @@ public class PatientIdPartitionInterceptor { } break; - + case EXTENDED_OPERATION_SERVER: + return provideNonPatientSpecificQueryResponse(theReadDetails); default: // nothing } + if (isBlank(theReadDetails.getResourceType())) { + return provideNonCompartmentMemberTypeResponse(null); + } + // If we couldn't identify a patient ID by the URL, let's try using the // conditional target if we have one if (theReadDetails.getConditionalTargetOrNull() != null) { @@ -172,42 +169,31 @@ public class PatientIdPartitionInterceptor { return provideNonPatientSpecificQueryResponse(theReadDetails); } - @Nonnull - private List getCompartmentSearchParams(RuntimeResourceDefinition resourceDef) { - return resourceDef.getSearchParams().stream() - .filter(param -> param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) - .filter(param -> param.getProvidesMembershipInCompartments() != null - && param.getProvidesMembershipInCompartments().contains("Patient")) - .collect(Collectors.toList()); - } - private List getResourceIdList( SearchParameterMap theParams, String theParamName, String theResourceType, boolean theExpectOnlyOneBool) { - List idParts = new ArrayList<>(); List> idParamAndList = theParams.get(theParamName); - if (idParamAndList != null) { - for (List idParamOrList : idParamAndList) { - for (IQueryParameterType idParam : idParamOrList) { - if (isNotBlank(idParam.getQueryParameterQualifier())) { - throw new MethodNotAllowedException( - Msg.code(1322) + "The parameter " + theParamName + idParam.getQueryParameterQualifier() - + " is not supported in patient compartment mode"); - } - if (idParam instanceof ReferenceParam) { - String chain = ((ReferenceParam) idParam).getChain(); - if (chain != null) { - throw new MethodNotAllowedException(Msg.code(1323) + "The parameter " + theParamName + "." - + chain + " is not supported in patient compartment mode"); - } - } + if (idParamAndList == null) { + return Collections.emptyList(); + } - IdType id = new IdType(idParam.getValueAsQueryToken(myFhirContext)); - if (!id.hasResourceType() || id.getResourceType().equals(theResourceType)) { - idParts.add(id.getIdPart()); - } + List idParts = new ArrayList<>(); + idParamAndList.stream().flatMap(Collection::stream).forEach(idParam -> { + if (isNotBlank(idParam.getQueryParameterQualifier())) { + throw new MethodNotAllowedException(Msg.code(1322) + "The parameter " + theParamName + + idParam.getQueryParameterQualifier() + " is not supported in patient compartment mode"); + } + if (idParam instanceof ReferenceParam) { + String chain = ((ReferenceParam) idParam).getChain(); + if (chain != null) { + throw new MethodNotAllowedException(Msg.code(1323) + "The parameter " + theParamName + "." + chain + + " is not supported in patient compartment mode"); } } - } + IdType id = new IdType(idParam.getValueAsQueryToken(myFhirContext)); + if (!id.hasResourceType() || id.getResourceType().equals(theResourceType)) { + idParts.add(id.getIdPart()); + } + }); if (theExpectOnlyOneBool && idParts.size() > 1) { throw new MethodNotAllowedException(Msg.code(1324) + "Multiple values for parameter " + theParamName diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java index 2547a97be9a..a98fb571d1a 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java @@ -94,10 +94,10 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition @Nonnull @Override public RequestPartitionId determineReadPartitionForRequest( - @Nullable RequestDetails theRequest, ReadPartitionIdRequestDetails theDetails) { + @Nullable RequestDetails theRequest, @Nonnull ReadPartitionIdRequestDetails theDetails) { RequestPartitionId requestPartitionId; - String resourceType = theDetails != null ? theDetails.getResourceType() : null; + String resourceType = theDetails.getResourceType(); boolean nonPartitionableResource = !isResourcePartitionable(resourceType); if (myPartitionSettings.isPartitioningEnabled()) { @@ -313,7 +313,7 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition @Override public void validateHasPartitionPermissions( - RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { + @Nonnull RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { if (myInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_SELECTED)) { RuntimeResourceDefinition runtimeResourceDefinition = null; if (theResourceType != null) { diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java index d06edb7d983..5f0a72d5171 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/searchparam/submit/interceptor/SearchParamValidatingInterceptor.java @@ -55,6 +55,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class SearchParamValidatingInterceptor { public static final String SEARCH_PARAM = "SearchParameter"; + public static final String SKIP_VALIDATION = SearchParamValidatingInterceptor.class.getName() + ".SKIP_VALIDATION"; private FhirContext myFhirContext; @@ -79,6 +80,14 @@ public class SearchParamValidatingInterceptor { if (isNotSearchParameterResource(theResource)) { return; } + + // avoid a loop when loading our hard-coded core FhirContext SearchParameters + boolean isStartup = theRequestDetails != null + && Boolean.TRUE == theRequestDetails.getUserData().get(SKIP_VALIDATION); + if (isStartup) { + return; + } + RuntimeSearchParam runtimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theResource); if (runtimeSearchParam == null) { return; diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMapTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMapTest.java new file mode 100644 index 00000000000..1bfb76dd375 --- /dev/null +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/IdSubstitutionMapTest.java @@ -0,0 +1,68 @@ +package ca.uhn.fhir.jpa.dao; + +import org.hl7.fhir.r4.model.IdType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class IdSubstitutionMapTest { + + private IdSubstitutionMap idSubstitutions; + + @BeforeEach + void setUp() { + idSubstitutions = new IdSubstitutionMap(); + } + + @ParameterizedTest + @CsvSource({ + "Patient/123/_history/3, Patient/123/_history/2", + "Patient/123/_history/3, Patient/123" + }) + void testUpdateTargets_inputMatchesTarget_onlyMatchedTargetUpdated(String theInputId, String theTargetId) { + idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType(theTargetId)); + idSubstitutions.put(new IdType("urn:uuid:5000"), new IdType("Patient/5000")); + idSubstitutions.put(new IdType("urn:uuid:6000"), new IdType("Patient/6000_history/3")); + + idSubstitutions.updateTargets(new IdType(theInputId)); + + assertEquals(theInputId, idSubstitutions.getForSource("urn:uuid:1234").getValue()); + assertEquals("Patient/5000", idSubstitutions.getForSource("urn:uuid:5000").getValue()); + assertEquals("Patient/6000_history/3", idSubstitutions.getForSource("urn:uuid:6000").getValue()); + } + + @Test + void testUpdateTargets_inputMatchesAllTargets_allTargetsUpdated() { + idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123/_history/1")); + idSubstitutions.put(new IdType("urn:uuid:5000"), new IdType("Patient/123/_history/2")); + idSubstitutions.put(new IdType("urn:uuid:6000"), new IdType("Patient/123/_history/4")); + + idSubstitutions.updateTargets(new IdType("Patient/123/_history/3")); + + assertEquals("Patient/123/_history/3", idSubstitutions.getForSource("urn:uuid:1234").getValue()); + assertEquals("Patient/123/_history/3", idSubstitutions.getForSource("urn:uuid:5000").getValue()); + assertEquals("Patient/123/_history/3", idSubstitutions.getForSource("urn:uuid:6000").getValue()); + } + + @ParameterizedTest + @ValueSource(strings = {"Patient/124", "Patient/124/_history/3", "Patient", ""}) + void testUpdateTargets_noMatchingTarget_noUpdate(String theInputId) { + idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123/_history/3")); + idSubstitutions.updateTargets(new IdType(theInputId)); + assertEquals("Patient/123/_history/3", idSubstitutions.getForSource("urn:uuid:1234").getValue()); + } + + @Test + void testUpdateTargets_nullInputId_noExceptionAndNoUpdate() { + idSubstitutions.put(new IdType("urn:uuid:1234"), new IdType("Patient/123/_history/3")); + idSubstitutions.updateTargets(null); + assertEquals("Patient/123/_history/3", idSubstitutions.getForSource("urn:uuid:1234").getValue()); + } +} diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 5b11dc8e881..5c1831f106d 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 9c0eb67b6d9..19b5e69d1f1 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 30eff20d18a..4d9804cfe5b 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java new file mode 100644 index 00000000000..ad71176a1f3 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.narrative2; + +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Medication; +import org.hl7.fhir.dstu3.model.Patient; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NarrativeGeneratorTemplateUtilsTest { + + @Test + public void testBundleHasEntriesWithResourceType_True() { + Bundle bundle = new Bundle(); + bundle.addEntry().setResource(new Patient().setActive(true)); + bundle.addEntry().setResource(new Medication().setIsBrand(true)); + assertTrue(NarrativeGeneratorTemplateUtils.INSTANCE.bundleHasEntriesWithResourceType(bundle, "Patient")); + } + + @Test + public void testBundleHasEntriesWithResourceType_False() { + Bundle bundle = new Bundle(); + bundle.addEntry().setResource(new Medication().setIsBrand(true)); + assertFalse(NarrativeGeneratorTemplateUtils.INSTANCE.bundleHasEntriesWithResourceType(bundle, "Patient")); + } + + +} diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 9bcf9ec681b..65378617379 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 8d3e9b2daf8..5c6456b96cf 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java index 7d4f4dc9092..b4cfd5404a7 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.IOException; import java.net.URL; +import ca.uhn.fhir.util.ClasspathUtil; import org.hl7.fhir.r4.model.Appointment; import org.hl7.fhir.r4.model.AuditEvent; import org.hl7.fhir.r4.model.Bundle; @@ -185,6 +186,19 @@ public class XmlParserR4Test extends BaseTest { assertEquals("12345", getPatientIdValue(bundle, 1)); } + @Test + public void testParseResource_withDecimalElementHasLeadingPlus_resourceParsedCorrectly() { + // setup + String text = ClasspathUtil.loadResource("observation-decimal-element-with-leading-plus.xml"); + + // execute + Observation observation = ourCtx.newXmlParser().parseResource(Observation.class, text); + + // verify + assertEquals("-3.0", observation.getReferenceRange().get(0).getLow().getValueElement().getValueAsString()); + assertEquals("3.0", observation.getReferenceRange().get(0).getHigh().getValueElement().getValueAsString()); + } + private String getPatientIdValue(Bundle input, int entry) { final DocumentReference documentReference = (DocumentReference)input.getEntry().get(entry).getResource(); final Patient patient = (Patient) documentReference.getSubject().getResource(); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/jsonlike/JsonLikeParserTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/jsonlike/JsonLikeParserTest.java index d240c74e804..c48495bcfde 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/jsonlike/JsonLikeParserTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/jsonlike/JsonLikeParserTest.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.parser.json.JsonLikeStructure; import ca.uhn.fhir.parser.json.jackson.JacksonStructure; import ca.uhn.fhir.parser.view.ExtPatient; import ca.uhn.fhir.util.AttachmentUtil; +import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; @@ -19,6 +20,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.ICompositeType; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.AfterAll; @@ -31,6 +33,7 @@ import java.io.StringReader; import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -84,6 +87,27 @@ public class JsonLikeParserTest { } + /** + * Test that json number values with a leading plus sign are parsed without exception. + * Previously, it was possible to save resources with leading plus sign numbers, e.g., "value": +3.0. + * To ensure that we could read such resources back, the ObjectMapper configuration was updated by enabling: + * {@link com.fasterxml.jackson.core.json.JsonReadFeature#ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS} + * Reproduces: https://github.com/hapifhir/hapi-fhir/issues/5667 + */ + @Test + public void testJsonLikeParser_resourceHasDecimalElementWithLeadingPlus_isParsedCorrectly() { + // setup + String text = ClasspathUtil.loadResource("observation-decimal-element-with-leading-plus.json"); + IJsonLikeParser jsonLikeParser = (IJsonLikeParser) ourCtx.newJsonParser(); + + // execute + IBaseResource resource = jsonLikeParser.parseResource(text); + + // validate + Observation observation = (Observation) resource; + assertEquals("3.0", observation.getReferenceRange().get(0).getHigh().getValueElement().getValueAsString()); + } + /** * Test JSON-Like writer using custom stream writer * diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java index ea057c97623..20059362e67 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java @@ -3,10 +3,12 @@ package ca.uhn.fhir.util.bundle; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.TestUtil; +import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -25,8 +27,9 @@ import org.hl7.fhir.r4.model.UriType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; -import jakarta.annotation.Nonnull; import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -555,6 +558,37 @@ public class BundleUtilTest { assertNull(actual); } + @ParameterizedTest + @CsvSource({ + // Actual BundleType Expected BundleTypeEnum + "TRANSACTION, TRANSACTION", + "DOCUMENT, DOCUMENT", + "MESSAGE, MESSAGE", + "BATCHRESPONSE, BATCH_RESPONSE", + "TRANSACTIONRESPONSE, TRANSACTION_RESPONSE", + "HISTORY, HISTORY", + "SEARCHSET, SEARCHSET", + "COLLECTION, COLLECTION" + }) + public void testGetBundleTypeEnum_withKnownBundleTypes_returnsCorrectBundleTypeEnum(Bundle.BundleType theBundleType, BundleTypeEnum theExpectedBundleTypeEnum){ + Bundle bundle = new Bundle(); + bundle.setType(theBundleType); + assertEquals(theExpectedBundleTypeEnum, BundleUtil.getBundleTypeEnum(ourCtx, bundle)); + } + + @Test + public void testGetBundleTypeEnum_withNullBundleType_returnsNull(){ + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.NULL); + assertNull(BundleUtil.getBundleTypeEnum(ourCtx, bundle)); + } + + @Test + public void testGetBundleTypeEnum_withNoBundleType_returnsNull(){ + Bundle bundle = new Bundle(); + assertNull(BundleUtil.getBundleTypeEnum(ourCtx, bundle)); + } + @Nonnull private static Bundle withBundle(Resource theResource) { final Bundle bundle = new Bundle(); diff --git a/hapi-fhir-structures-r4/src/test/resources/observation-decimal-element-with-leading-plus.json b/hapi-fhir-structures-r4/src/test/resources/observation-decimal-element-with-leading-plus.json new file mode 100644 index 00000000000..221d88ed142 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/observation-decimal-element-with-leading-plus.json @@ -0,0 +1,64 @@ +{ + "resourceType": "Observation", + "id": "1355", + "meta": { + "versionId": "1", + "lastUpdated": "2024-02-02T10:00:08.286-07:00", + "source": "#rxekXV5y3UZKAwxF" + }, + "identifier": [ + { + "system": "http://TEST.nl/fhir/NamingSystem/laboratory_observations", + "value": "H_2211280010-BEART" + } + ], + "status": "preliminary", + "category": [ + { + "coding": [ + { + "system": "5-Step briefing | SNOMED International", + "code": "49581000146104", + "display": "Laboratory test finding (finding)" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "Home – LOINC", + "code": "8889-8", + "display": "Heart rate by Pulse oximeter" + } + ] + }, + "subject": { + "reference": "Patient/1354", + "display": "Jongetje. Jongensnaamnaam van" + }, + "effectiveDateTime": "2022-11-28T16:09:00+01:00", + "performer": [ + { + "reference": "Practitioner/P8", + "display": "TEST" + } + ], + "referenceRange": [ + { + "low": { + "value": -3.0, + "unit": "mmol/l", + "system": "Home", + "code": "mmol/l" + }, + "high": { + "value": +3.0, + "unit": "mmol/l", + "system": "Home", + "code": "mmol/l" + }, + "text": "-3.0 mmol/l - +3.0 mmol/l" + } + ] +} diff --git a/hapi-fhir-structures-r4/src/test/resources/observation-decimal-element-with-leading-plus.xml b/hapi-fhir-structures-r4/src/test/resources/observation-decimal-element-with-leading-plus.xml new file mode 100644 index 00000000000..4c7de31dfec --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/resources/observation-decimal-element-with-leading-plus.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index 4e128170534..9df5225d9be 100644 --- a/hapi-fhir-structures-r4b/pom.xml +++ b/hapi-fhir-structures-r4b/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 7230b2d5974..820a2694472 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 1b2713493d4..1910352fb87 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index cf420ee8e11..d206f2a4f62 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index db0343305da..96aea76d2ec 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index e08aaffcd09..423eabd94d3 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 595a06b7d75..15cba6829a5 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index 98966449e16..7f5a9368dc9 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4b/pom.xml b/hapi-fhir-validation-resources-r4b/pom.xml index 2ce153ceca0..bbbb4be0bf6 100644 --- a/hapi-fhir-validation-resources-r4b/pom.xml +++ b/hapi-fhir-validation-resources-r4b/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 9024474ad68..05b3a32a8e2 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 4d0c094552f..99f2eba5514 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index f61b849e948..7edb7effd39 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -254,6 +254,13 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple Optional result = (Optional) theCache.get(theKey, loaderWrapper); assert result != null; + // UGH! Animal sniffer :( + if (!result.isPresent()) { + ourLog.debug( + "Invalidating cache entry for key: {} since the result of the underlying query is empty", theKey); + theCache.invalidate(theKey); + } + return result.orElse(null); } 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 4d1f2323243..7ecbec36fc0 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 @@ -28,6 +28,7 @@ import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_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.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; import org.slf4j.Logger; @@ -408,21 +409,27 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { @Override public IBaseResource fetchCodeSystem(String theSystem) { - + final CodeSystemContentMode content; Map map; switch (defaultString(theSystem)) { case COUNTRIES_CODESYSTEM_URL: map = ISO_3166_CODES; + content = CodeSystemContentMode.COMPLETE; break; case CURRENCIES_CODESYSTEM_URL: map = ISO_4217_CODES; + content = CodeSystemContentMode.COMPLETE; + break; + case MIMETYPES_CODESYSTEM_URL: + map = Collections.emptyMap(); + content = CodeSystemContentMode.NOTPRESENT; break; default: return null; } CodeSystem retVal = new CodeSystem(); - retVal.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + retVal.setContent(content); retVal.setUrl(theSystem); for (Map.Entry nextEntry : map.entrySet()) { retVal.addConcept().setCode(nextEntry.getKey()).setDisplay(nextEntry.getValue()); 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 65fbda9c56a..ff8e442b1cc 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 @@ -745,13 +745,6 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu return expandValueSetR5(theValidationSupportContext, input, theWantSystemUrlAndVersion, theWantCode); } - @Nullable - private org.hl7.fhir.r5.model.ValueSet expandValueSetR5( - ValidationSupportContext theValidationSupportContext, org.hl7.fhir.r5.model.ValueSet theInput) - throws ExpansionCouldNotBeCompletedInternallyException { - return expandValueSetR5(theValidationSupportContext, theInput, null, null); - } - @Nullable private org.hl7.fhir.r5.model.ValueSet expandValueSetR5( ValidationSupportContext theValidationSupportContext, @@ -909,20 +902,25 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu includeOrExcludeSystemResource = codeSystemLoader.apply(loadedCodeSystemUrl); - Set wantCodes; - if (theInclude.getConcept().isEmpty()) { - wantCodes = null; + boolean isIncludeWithDeclaredConcepts = !theInclude.getConcept().isEmpty(); + + final Set wantCodes; + if (isIncludeWithDeclaredConcepts) { + wantCodes = theInclude.getConcept().stream() + .map(org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent::getCode) + .collect(Collectors.toSet()); } else { - wantCodes = - theInclude.getConcept().stream().map(t -> t.getCode()).collect(Collectors.toSet()); + wantCodes = null; } boolean ableToHandleCode = false; String failureMessage = null; FailureType failureType = FailureType.OTHER; - if (includeOrExcludeSystemResource == null - || includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT) { + boolean isIncludeCodeSystemIgnored = includeOrExcludeSystemResource != null + && includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT; + + if (includeOrExcludeSystemResource == null || isIncludeCodeSystemIgnored) { if (theWantCode != null) { if (theValidationSupportContext @@ -971,7 +969,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu // If the ValueSet.compose.include has no individual concepts in it, and // we can't find the actual referenced CodeSystem, we have no choice // but to fail - if (!theInclude.getConcept().isEmpty()) { + if (isIncludeWithDeclaredConcepts) { ableToHandleCode = true; } else { failureMessage = getFailureMessageForMissingOrUnusableCodeSystem( @@ -998,15 +996,22 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } } } else { - if (isNotBlank(theInclude.getSystem()) - && !theInclude.getConcept().isEmpty() - && theInclude.getFilter().isEmpty() - && theInclude.getValueSet().isEmpty()) { - theInclude.getConcept().stream() - .map(t -> new FhirVersionIndependentConcept( - theInclude.getSystem(), t.getCode(), t.getDisplay(), theInclude.getVersion())) - .forEach(t -> nextCodeList.add(t)); - ableToHandleCode = true; + boolean isIncludeFromSystem = isNotBlank(theInclude.getSystem()) + && theInclude.getValueSet().isEmpty(); + boolean isIncludeWithFilter = !theInclude.getFilter().isEmpty(); + if (isIncludeFromSystem && !isIncludeWithFilter) { + if (isIncludeWithDeclaredConcepts) { + theInclude.getConcept().stream() + .map(t -> new FhirVersionIndependentConcept( + theInclude.getSystem(), + t.getCode(), + t.getDisplay(), + theInclude.getVersion())) + .forEach(nextCodeList::add); + ableToHandleCode = true; + } else if (isIncludeCodeSystemIgnored) { + ableToHandleCode = true; + } } if (!ableToHandleCode) { 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 9bb0e691dd1..19402063574 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 @@ -14,6 +14,7 @@ import ca.uhn.fhir.rest.api.EncodingEnum; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r5.model.Enumerations; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -189,6 +190,7 @@ public class CommonCodeSystemsTerminologyServiceTest extends BaseValidationTestW org.hl7.fhir.r5.model.CodeSystem cs = (org.hl7.fhir.r5.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); assertNotNull(cs); assertEquals(498, cs.getConcept().size()); + assertEquals(Enumerations.CodeSystemContentMode.COMPLETE, cs.getContent()); } @Test @@ -300,7 +302,8 @@ public class CommonCodeSystemsTerminologyServiceTest extends BaseValidationTestW @Test public void testFetchCodeSystem_withMimeType_returnsOk() { CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(MIMETYPES_CODESYSTEM_URL); - assertNull(cs); + assertTrue(cs.getConcept().isEmpty()); + assertEquals(CodeSystem.CodeSystemContentMode.NOTPRESENT, cs.getContent()); } @ParameterizedTest 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 index 77d309503b6..1b5de1d4a50 100644 --- 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 @@ -17,8 +17,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; @@ -33,10 +31,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class InMemoryTerminologyServerValidationSupportTest extends BaseValidationTestWithInlineMocks { - - private static final Logger ourLog = LoggerFactory.getLogger(InMemoryTerminologyServerValidationSupportTest.class); private InMemoryTerminologyServerValidationSupport mySvc; - private FhirContext myCtx = FhirContext.forR4(); + private final FhirContext myCtx = FhirContext.forR4(); private DefaultProfileValidationSupport myDefaultSupport; private ValidationSupportChain myChain; private PrePopulatedValidationSupport myPrePopulated; @@ -54,8 +50,153 @@ public class InMemoryTerminologyServerValidationSupportTest extends BaseValidati myDefaultSupport.fetchCodeSystem("http://foo"); } + @ParameterizedTest + @ValueSource(strings = { + CommonCodeSystemsTerminologyService.MIMETYPES_VALUESET_URL, + CommonCodeSystemsTerminologyService.CURRENCIES_VALUESET_URL, + CommonCodeSystemsTerminologyService.LANGUAGES_VALUESET_URL + }) + public void testExpandValueSet_commonVS_expandOk(String theValueSet) { + ValueSet vs = (ValueSet) myChain.fetchValueSet(theValueSet); + assertNotNull(vs); + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + + IValidationSupport.ValueSetExpansionOutcome expansion = mySvc.expandValueSet(valCtx, new ValueSetExpansionOptions(), vs); + assertNotNull(expansion); + assertNull(expansion.getError()); + ValueSet valueSet = (ValueSet) expansion.getValueSet(); + assertNotNull(valueSet); + assertNotNull(valueSet.getExpansion()); + } + + + @ParameterizedTest + @ValueSource(strings = { + CommonCodeSystemsTerminologyService.MIMETYPES_CODESYSTEM_URL, + CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL, + CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL + }) + public void testExpandValueSet_customVSBasedOnCommonCS_expandOk(String theCodeSystem) { + ValueSet vs = new ValueSet(); + vs.setId("mimetype"); + vs.setUrl("http://example.com/mimetype"); + vs.setVersion("1.0"); + vs.setStatus(Enumerations.PublicationStatus.ACTIVE); + ValueSet.ConceptSetComponent vsInclude = vs.getCompose().addInclude(); + vsInclude.setSystem(theCodeSystem); + myPrePopulated.addValueSet(vs); + + vs = (ValueSet) myChain.fetchValueSet(vs.getUrl()); + assertNotNull(vs); + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + + IValidationSupport.ValueSetExpansionOutcome expansion = mySvc.expandValueSet(valCtx, new ValueSetExpansionOptions(), vs); + assertNotNull(expansion); + assertNull(expansion.getError()); + ValueSet valueSet = (ValueSet) expansion.getValueSet(); + assertNotNull(valueSet); + assertNotNull(valueSet.getExpansion()); + } + @Test - public void testValidateCodeWithInferredSystem_CommonCodeSystemsCs_BuiltInVs() { + public void testValidateCode_mimetypeVSRandomCode_returnsOk() { + final String codeSystem = CommonCodeSystemsTerminologyService.MIMETYPES_CODESYSTEM_URL; + final String valueSetUrl = CommonCodeSystemsTerminologyService.MIMETYPES_VALUESET_URL; + + final String code = "someRandomCode"; + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + ConceptValidationOptions options = new ConceptValidationOptions(); + + IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(valCtx, options, codeSystem, code, null, valueSetUrl); + assertNotNull(outcome); + assertTrue(outcome.isOk()); + assertEquals(code, outcome.getCode()); + } + + @Test + public void testValidateCode_customMimetypeVSRandomCode_returnsOk() { + final String codeSystem = CommonCodeSystemsTerminologyService.MIMETYPES_CODESYSTEM_URL; + final String code = "someRandomCode"; + + ValueSet vs = new ValueSet(); + vs.setId("mimetype"); + vs.setUrl("http://example.com/mimetype"); + vs.setVersion("1.0"); + vs.setStatus(Enumerations.PublicationStatus.ACTIVE); + ValueSet.ConceptSetComponent vsInclude = vs.getCompose().addInclude(); + vsInclude.setSystem(codeSystem); + myPrePopulated.addValueSet(vs); + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + ConceptValidationOptions options = new ConceptValidationOptions(); + + IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(valCtx, options, codeSystem, code, null, vs.getUrl()); + assertNotNull(outcome); + assertTrue(outcome.isOk()); + } + + @Test + public void testValidateCode_customMimetypeVSCodeInVS_returnsOk() { + String codeSystem = CommonCodeSystemsTerminologyService.MIMETYPES_CODESYSTEM_URL; + + final String code = "someRandomCode"; + final String display = "Display " + code; + + ValueSet vs = new ValueSet(); + vs.setId("example-vs"); + vs.setUrl("http://example.com/example-vs"); + vs.setVersion("1.0"); + vs.setStatus(Enumerations.PublicationStatus.ACTIVE); + ValueSet.ConceptSetComponent vsInclude = vs.getCompose().addInclude(); + vsInclude.setSystem(codeSystem); + vsInclude.addConcept().setCode(code).setDisplay(display); + myPrePopulated.addValueSet(vs); + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + ConceptValidationOptions options = new ConceptValidationOptions(); + + IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(valCtx, options, codeSystem, code, null, vs.getUrl()); + assertNotNull(outcome); + assertTrue(outcome.isOk()); + assertEquals(code, outcome.getCode()); + } + + @ParameterizedTest + @ValueSource(strings = { + CommonCodeSystemsTerminologyService.MIMETYPES_CODESYSTEM_URL, + CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL, + CommonCodeSystemsTerminologyService.CURRENCIES_VALUESET_URL, + CommonCodeSystemsTerminologyService.LANGUAGES_CODESYSTEM_URL, + CommonCodeSystemsTerminologyService.UCUM_CODESYSTEM_URL + }) + public void testValidateCode_customMimetypeVSCodeNotInVS_returnsError(String theCodeSystem) { + final String code = "someRandomCode"; + final String codeToValidate = "otherCode"; + + ValueSet vs = new ValueSet(); + vs.setId("mimetype"); + vs.setUrl("http://example.com/mimetype"); + vs.setVersion("1.0"); + vs.setStatus(Enumerations.PublicationStatus.ACTIVE); + ValueSet.ConceptSetComponent vsInclude = vs.getCompose().addInclude(); + vsInclude.setSystem(theCodeSystem); + vsInclude.addConcept().setCode(code).setDisplay("Display " + code); + myPrePopulated.addValueSet(vs); + + ValidationSupportContext valCtx = new ValidationSupportContext(myChain); + ConceptValidationOptions options = new ConceptValidationOptions(); + + IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(valCtx, options, theCodeSystem, codeToValidate, null, vs.getUrl()); + assertNotNull(outcome); + assertFalse(outcome.isOk()); + assertEquals("Unknown code '" + theCodeSystem + "#" + codeToValidate + "' for in-memory expansion of ValueSet '" + vs.getUrl() + "'", outcome.getMessage()); + } + + @Test + public void testValidateCodeWithInferredSystem_CommonCs_BuiltInVs() { ValidationSupportContext valCtx = new ValidationSupportContext(myChain); ConceptValidationOptions options = new ConceptValidationOptions().setInferSystem(true); @@ -279,9 +420,6 @@ public class InMemoryTerminologyServerValidationSupportTest extends BaseValidati assertEquals(null, outcome.getCodeSystemVersion()); } - - - @Test public void testExpandValueSet_VsUsesVersionedSystem_CsIsFragmentWithoutCode() { CodeSystem cs = new CodeSystem(); diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 8459ead2709..b643102aa54 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index fa628d257f8..2350f749d3b 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 4b3821e0418..10759febe80 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. @@ -674,6 +674,11 @@ Trifork Martin Zacho Grønhøj + + nigtrifork + Trifork + Nicolai Gjøderum + augla August Langhout diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 05def91802e..707f6d354ec 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 340b817cf11..4d21b0c7eb9 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index 7aaa8542430..2841ee1aff4 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.1.0-SNAPSHOT + 7.1.3-SNAPSHOT ../../pom.xml
Vital Signs
Code
CodeResultUnitInterpretationCommentsDateCodeResultUnitInterpretationCommentsDate