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 2b8692c65ba..a9387bf6621 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 @@ -25,6 +25,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 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.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -36,6 +37,7 @@ import ca.uhn.fhir.util.bundle.ModifiableBundleEntry; import ca.uhn.fhir.util.bundle.SearchBundleEntryParts; import com.google.common.collect.Sets; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.apache.commons.lang3.tuple.Pair; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBinary; @@ -56,6 +58,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_PREV; @@ -642,6 +645,41 @@ public class BundleUtil { return retVal; } + public static IBase getReferenceInBundle( + @Nonnull FhirContext theFhirContext, @Nonnull String theUrl, @Nullable Object theAppContext) { + if (!(theAppContext instanceof IBaseBundle) || isBlank(theUrl) || theUrl.startsWith("#")) { + return null; + } + + /* + * If this is a reference that is a UUID, we must be looking for local references within a Bundle + */ + IBaseBundle bundle = (IBaseBundle) theAppContext; + + final boolean isPlaceholderReference = theUrl.startsWith("urn:"); + final String unqualifiedVersionlessReference = + new IdDt(theUrl).toUnqualifiedVersionless().getValue(); + + for (BundleEntryParts next : BundleUtil.toListOfEntries(theFhirContext, bundle)) { + IBaseResource nextResource = next.getResource(); + if (nextResource == null) { + continue; + } + if (isPlaceholderReference) { + if (theUrl.equals(next.getUrl()) + || theUrl.equals(nextResource.getIdElement().getValue())) { + return nextResource; + } + } else { + if (unqualifiedVersionlessReference.equals( + nextResource.getIdElement().toUnqualifiedVersionless().getValue())) { + return nextResource; + } + } + } + return null; + } + /** * DSTU3 did not allow the PATCH verb for transaction bundles- so instead we infer that a bundle * is a patch if the payload is a binary resource containing a patch. This method 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 8c379e651ce..791401008c3 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 @@ -130,6 +130,8 @@ public enum VersionEnum { V6_10_0, V6_10_1, + V6_10_2, + V6_10_3, V6_11_0, V7_0_0; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_1/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_1/version.yaml index a29e249409a..516f091f11f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_1/version.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_1/version.yaml @@ -1,3 +1,3 @@ --- -release-date: "2023-08-31" +release-date: "2023-12-18" codename: "Zed" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_2/upgrade.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_2/upgrade.md new file mode 100644 index 00000000000..6d06ae7f250 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_2/upgrade.md @@ -0,0 +1,5 @@ +### Major Database Change + +This release fixes a migration from 6.10.1 that was ineffective for SQL Server (MSSQL) instances. +This may take several minutes on a larger system (e.g. 10 minutes for 100 million resources). +For zero-downtime, or for larger systems, we recommend you upgrade the schema using the CLI tools. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_2/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_2/version.yaml new file mode 100644 index 00000000000..01b8caebed4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_10_2/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2023-12-22" +codename: "Zed" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5465-openapi-enhancements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5465-openapi-enhancements.yaml new file mode 100644 index 00000000000..30f80d356b6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5465-openapi-enhancements.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5465 +title: "Several fixes to the HAPI FHIR generated OpenAPI schema have been implemented. This means that + the spec now validates cleanly. Thanks to Primož Delopst for the contribution!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5564-fhirpath-expression-with-resolve-doesnt-work-for-bundle.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5564-fhirpath-expression-with-resolve-doesnt-work-for-bundle.yaml new file mode 100644 index 00000000000..a47e1c698bb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5564-fhirpath-expression-with-resolve-doesnt-work-for-bundle.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5564 +title: "Previously, FHIRPath expression evaluation when using the `_fhirpath` parameter would not work on chained +use of 'resolve()'. This was most notable when using `_fhirpath` with FHIR Documents (i.e. 'Bundle' of type 'document' +where 'entry[0]' is a 'Composition'). This has now been fixed." 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 cd577a6d4c5..9d50199fb88 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 @@ -198,7 +198,11 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { "Column HFJ_SPIDX_STRING.SP_VALUE_NORMALIZED already has a collation of 'C' so doing nothing"); } - version.addTask(new ForceIdMigrationFixTask(version.getRelease(), "20231213.1")); + // This fix was bad for MSSQL, it has been set to do nothing. + version.addTask(new ForceIdMigrationFixTask(version.getRelease(), "20231213.1").setDoNothing(true)); + + // This fix will work for MSSQL or Oracle. + version.addTask(new ForceIdMigrationFixTask(version.getRelease(), "20231222.1")); } protected void init680() { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java index 223a254e577..863c8f734bc 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java @@ -41,12 +41,10 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; -import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.StringUtil; import ca.uhn.fhir.util.UrlUtil; -import ca.uhn.fhir.util.bundle.BundleEntryParts; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import jakarta.annotation.Nonnull; @@ -59,14 +57,12 @@ import org.apache.commons.text.StringTokenizer; import org.fhir.ucum.Pair; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseEnumeration; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.IdType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -2004,47 +2000,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } } - @SuppressWarnings("unchecked") - protected final T resolveResourceInBundleWithPlaceholderId(Object theAppContext, String theUrl) { - /* - * If this is a reference that is a UUID, we must be looking for local - * references within a Bundle - */ - if (theAppContext instanceof IBaseBundle && isNotBlank(theUrl) && !theUrl.startsWith("#")) { - String unqualifiedVersionlessReference; - boolean isPlaceholderReference; - if (theUrl.startsWith("urn:")) { - isPlaceholderReference = true; - unqualifiedVersionlessReference = null; - } else { - isPlaceholderReference = false; - unqualifiedVersionlessReference = - new IdType(theUrl).toUnqualifiedVersionless().getValue(); - } - - List entries = BundleUtil.toListOfEntries(getContext(), (IBaseBundle) theAppContext); - for (BundleEntryParts next : entries) { - if (next.getResource() != null) { - if (isPlaceholderReference) { - if (theUrl.equals(next.getUrl()) - || theUrl.equals( - next.getResource().getIdElement().getValue())) { - return (T) next.getResource(); - } - } else { - if (unqualifiedVersionlessReference.equals(next.getResource() - .getIdElement() - .toUnqualifiedVersionless() - .getValue())) { - return (T) next.getResource(); - } - } - } - } - } - return null; - } - @FunctionalInterface public interface IValueExtractor { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java index 3dcad8c3347..ebddca35be0 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.sl.cache.Cache; import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.util.BundleUtil; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.PostConstruct; import org.hl7.fhir.exceptions.FHIRException; @@ -150,9 +151,8 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements } @Override - public Base resolveReference(FHIRPathEngine engine, Object theAppContext, String theUrl, Base refContext) - throws FHIRException { - Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl); + public Base resolveReference(FHIRPathEngine engine, Object theAppContext, String theUrl, Base refContext) { + Base retVal = (Base) BundleUtil.getReferenceInBundle(getContext(), theUrl, theAppContext); if (retVal != null) { return retVal; } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java index 8a67587269b..f804e770cd1 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4B.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.sl.cache.Cache; import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.util.BundleUtil; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.PostConstruct; import org.hl7.fhir.exceptions.FHIRException; @@ -150,9 +151,8 @@ public class SearchParamExtractorR4B extends BaseSearchParamExtractor implements } @Override - public Base resolveReference(FHIRPathEngine engine, Object theAppContext, String theUrl, Base refContext) - throws FHIRException { - Base retVal = resolveResourceInBundleWithPlaceholderId(theAppContext, theUrl); + public Base resolveReference(FHIRPathEngine engine, Object theAppContext, String theUrl, Base refContext) { + Base retVal = (Base) BundleUtil.getReferenceInBundle(getContext(), theUrl, theAppContext); if (retVal != null) { return retVal; } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java index 1e931500a7c..dd99d856543 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR5.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.sl.cache.Cache; import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.util.BundleUtil; import jakarta.annotation.PostConstruct; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.PathEngineException; @@ -147,9 +148,8 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements } @Override - public Base resolveReference(FHIRPathEngine engine, Object appContext, String theUrl, Base refContext) - throws FHIRException { - Base retVal = resolveResourceInBundleWithPlaceholderId(appContext, theUrl); + public Base resolveReference(FHIRPathEngine engine, Object appContext, String theUrl, Base refContext) { + Base retVal = (Base) BundleUtil.getReferenceInBundle(getContext(), theUrl, appContext); if (retVal != null) { return retVal; } diff --git a/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java index 5c3bde714eb..a495e074b92 100644 --- a/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java +++ b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.UrlUtil; +import com.google.common.collect.ImmutableList; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import io.swagger.v3.core.util.Yaml; @@ -47,9 +48,13 @@ import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.DateSchema; +import io.swagger.v3.oas.models.media.DateTimeSchema; import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.NumberSchema; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; @@ -73,6 +78,7 @@ import org.hl7.fhir.r4.model.CapabilityStatement; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.OperationDefinition; @@ -109,6 +115,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.function.Supplier; @@ -679,9 +686,10 @@ public class OpenApiInterceptor { operation.addParametersItem(parametersItem); parametersItem.setName(nextSearchParam.getName()); + parametersItem.setRequired(false); parametersItem.setIn("query"); parametersItem.setDescription(nextSearchParam.getDocumentation()); - parametersItem.setStyle(Parameter.StyleEnum.SIMPLE); + parametersItem.setSchema(toSchema(nextSearchParam.getType())); } } @@ -765,7 +773,13 @@ public class OpenApiInterceptor { "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET); populateOperation( - theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true); + theFhirContext, + theOpenApi, + theResourceType, + operationDefinition, + operation, + "/" + theResourceType + "/$" + operationDefinition.getCode(), + PathItem.HttpMethod.GET); } if (operationDefinition.getInstance()) { Operation operation = getPathItem( @@ -774,13 +788,26 @@ public class OpenApiInterceptor { PathItem.HttpMethod.GET); addResourceIdParameter(operation); populateOperation( - theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true); + theFhirContext, + theOpenApi, + theResourceType, + operationDefinition, + operation, + "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), + PathItem.HttpMethod.GET); } } else { if (operationDefinition.getSystem()) { Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET); - populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, true); + populateOperation( + theFhirContext, + theOpenApi, + null, + operationDefinition, + operation, + "/$" + operationDefinition.getCode(), + PathItem.HttpMethod.GET); } } } @@ -793,7 +820,13 @@ public class OpenApiInterceptor { "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST); populateOperation( - theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false); + theFhirContext, + theOpenApi, + theResourceType, + operationDefinition, + operation, + "/" + theResourceType + "/$" + operationDefinition.getCode(), + PathItem.HttpMethod.POST); } if (operationDefinition.getInstance()) { Operation operation = getPathItem( @@ -802,13 +835,26 @@ public class OpenApiInterceptor { PathItem.HttpMethod.POST); addResourceIdParameter(operation); populateOperation( - theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false); + theFhirContext, + theOpenApi, + theResourceType, + operationDefinition, + operation, + "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), + PathItem.HttpMethod.POST); } } else { if (operationDefinition.getSystem()) { Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST); - populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, false); + populateOperation( + theFhirContext, + theOpenApi, + null, + operationDefinition, + operation, + "/$" + operationDefinition.getCode(), + PathItem.HttpMethod.POST); } } } @@ -847,16 +893,18 @@ public class OpenApiInterceptor { String theResourceType, OperationDefinition theOperationDefinition, Operation theOperation, - boolean theGet) { + String thePath, + PathItem.HttpMethod httpMethod) { if (theResourceType == null) { theOperation.addTagsItem(PAGE_SYSTEM); } else { theOperation.addTagsItem(theResourceType); } - theOperation.setSummary(theOperationDefinition.getTitle()); + theOperation.setSummary(Optional.ofNullable(theOperationDefinition.getTitle()) + .orElse(String.format("%s: %s", httpMethod.name(), thePath))); theOperation.setDescription(theOperationDefinition.getDescription()); addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null); - if (theGet) { + if (httpMethod == PathItem.HttpMethod.GET) { for (OperationDefinition.OperationDefinitionParameterComponent nextParameter : theOperationDefinition.getParameter()) { @@ -873,8 +921,8 @@ public class OpenApiInterceptor { parametersItem.setName(nextParameter.getName()); parametersItem.setIn("query"); parametersItem.setDescription(nextParameter.getDocumentation()); - parametersItem.setStyle(Parameter.StyleEnum.SIMPLE); parametersItem.setRequired(nextParameter.getMin() > 0); + parametersItem.setSchema(toSchema(nextParameter.getSearchType())); List exampleExtensions = nextParameter.getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); @@ -1024,6 +1072,7 @@ public class OpenApiInterceptor { parameter.setIn("path"); parameter.setDescription("The resource version ID"); parameter.setExample("1"); + parameter.setRequired(true); parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1))); parameter.setStyle(Parameter.StyleEnum.SIMPLE); theOperation.addParametersItem(parameter); @@ -1085,6 +1134,7 @@ public class OpenApiInterceptor { parameter.setIn("path"); parameter.setDescription("The resource ID"); parameter.setExample("123"); + parameter.setRequired(true); parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1))); parameter.setStyle(Parameter.StyleEnum.SIMPLE); theOperation.addParametersItem(parameter); @@ -1188,4 +1238,31 @@ public class OpenApiInterceptor { return builder.toString(); } } + + private Schema toSchema(Enumerations.SearchParamType type) { + if (type == null) { + return new StringSchema(); + } + switch (type) { + case NUMBER: + return new NumberSchema(); + case DATE: + Schema dateSchema = new Schema<>(); + dateSchema.anyOf(ImmutableList.of(new DateTimeSchema(), new DateSchema())); + return dateSchema; + case QUANTITY: + Schema quantitySchema = new Schema<>(); + quantitySchema.anyOf(ImmutableList.of(new StringSchema(), new NumberSchema())); + return quantitySchema; + case STRING: + case TOKEN: + case REFERENCE: + case COMPOSITE: + case URI: + case SPECIAL: + case NULL: + default: + return new StringSchema(); + } + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptor.java index 25433e98f77..8748bf38bd0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptor.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.server.interceptor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.fhirpath.FhirPathExecutionException; import ca.uhn.fhir.fhirpath.IFhirPath; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; @@ -29,10 +30,14 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.ResponseDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.ParametersUtil; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import java.util.List; @@ -65,6 +70,12 @@ public class FhirPathFilterInterceptor { ParametersUtil.addPartString(ctx, resultPart, "expression", expression); IFhirPath fhirPath = ctx.newFhirPath(); + fhirPath.setEvaluationContext(new IFhirPathEvaluationContext() { + @Override + public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { + return BundleUtil.getReferenceInBundle(ctx, theReference.getValue(), responseResource); + } + }); List outputs; try { outputs = fhirPath.evaluate(responseResource, expression, IBase.class); diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ForceIdMigrationFixTask.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ForceIdMigrationFixTask.java index c6eca73bb9b..7f29d3ba38b 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ForceIdMigrationFixTask.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ForceIdMigrationFixTask.java @@ -100,7 +100,7 @@ public class ForceIdMigrationFixTask extends BaseTask { + // avoid useless updates on engines that don't check // skip case 1, 2. Only check 3,4,5 - " where (fhir_id is null or fhir_id <> trim(fhir_id)) " + getWhereClauseByDBType() + // chunk range. " and res_id >= ? and res_id < ?", @@ -109,6 +109,15 @@ public class ForceIdMigrationFixTask extends BaseTask { } } + private String getWhereClauseByDBType() { + switch (getDriverType()) { + case MSSQL_2012: + return " where (fhir_id is null or DATALENGTH(fhir_id) > LEN(fhir_id)) "; + default: + return " where (fhir_id is null or fhir_id <> trim(fhir_id)) "; + } + } + @Override protected void generateHashCode(HashCodeBuilder theBuilder) { // no-op - this is a singleton. diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorR4Test.java similarity index 54% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorR4Test.java index bdc8a63bed9..d39df163190 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorR4Test.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.rest.server.interceptor; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.test.utilities.HttpClientExtension; @@ -13,40 +12,63 @@ import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; +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.CodeableConcept; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -public class FhirPathFilterInterceptorTest { +public class FhirPathFilterInterceptorR4Test { - private static final Logger ourLog = LoggerFactory.getLogger(FhirPathFilterInterceptorTest.class); + private static final Logger ourLog = LoggerFactory.getLogger(FhirPathFilterInterceptorR4Test.class); private static FhirContext ourCtx = FhirContext.forR4(); + @Order(0) @RegisterExtension public HttpClientExtension myHttpClientExtension = new HttpClientExtension(); + @Order(0) @RegisterExtension public RestfulServerExtension myServerExtension = new RestfulServerExtension(ourCtx); + @Order(1) @RegisterExtension - public HashMapResourceProviderExtension myProviderExtension = new HashMapResourceProviderExtension<>(myServerExtension, Patient.class); + public HashMapResourceProviderExtension myPatientProvider = new HashMapResourceProviderExtension<>(myServerExtension, Patient.class); + @Order(1) + @RegisterExtension + public HashMapResourceProviderExtension myMedicationAdministrationProvider = new HashMapResourceProviderExtension<>(myServerExtension, MedicationAdministration.class); + @Order(1) + @RegisterExtension + public HashMapResourceProviderExtension myBundleProvider = new HashMapResourceProviderExtension<>(myServerExtension, Bundle.class); private IGenericClient myClient; private String myBaseUrl; private CloseableHttpClient myHttpClient; - private IIdType myPatientId; @BeforeEach public void before() { - myProviderExtension.clear(); myServerExtension.getRestfulServer().getInterceptorService().unregisterAllInterceptors(); myServerExtension.getRestfulServer().getInterceptorService().registerInterceptor(new FhirPathFilterInterceptor()); @@ -57,9 +79,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testUnfilteredResponse() throws IOException { - createPatient(); + IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId.getValue()); + HttpGet request = new HttpGet(patientId.getValue()); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -72,9 +94,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testUnfilteredResponse_WithResponseHighlightingInterceptor() throws IOException { myServerExtension.getRestfulServer().registerInterceptor(new ResponseHighlighterInterceptor()); - createPatient(); + final IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId.getValue() + "?_format=" + Constants.FORMATS_HTML_JSON); + HttpGet request = new HttpGet(patientId.getValue() + "?_format=" + Constants.FORMATS_HTML_JSON); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -85,9 +107,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testFilteredResponse() throws IOException { - createPatient(); + final IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient.identifier&_pretty=true"); + HttpGet request = new HttpGet(patientId + "?_fhirpath=Patient.identifier&_pretty=true"); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -99,9 +121,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testFilteredResponse_ExpressionReturnsExtension() throws IOException { - createPatient(); + final IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient.extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-race')&_pretty=true"); + HttpGet request = new HttpGet(patientId + "?_fhirpath=Patient.extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-race')&_pretty=true"); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -112,9 +134,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testFilteredResponse_ExpressionReturnsResource() throws IOException { - createPatient(); + final IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient&_pretty=true"); + HttpGet request = new HttpGet(patientId + "?_fhirpath=Patient&_pretty=true"); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -127,9 +149,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testFilteredResponse_ExpressionIsInvalid() throws IOException { - createPatient(); + final IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId + "?_fhirpath=" + UrlUtil.escapeUrlParam("***")); + HttpGet request = new HttpGet(patientId + "?_fhirpath=" + UrlUtil.escapeUrlParam("***")); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -160,9 +182,9 @@ public class FhirPathFilterInterceptorTest { @Test public void testFilteredResponse_WithResponseHighlightingInterceptor() throws IOException { myServerExtension.getRestfulServer().registerInterceptor(new ResponseHighlighterInterceptor()); - createPatient(); + final IIdType patientId = createPatient(); - HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient.identifier&_format=" + Constants.FORMATS_HTML_JSON); + HttpGet request = new HttpGet(patientId + "?_fhirpath=Patient.identifier&_format=" + Constants.FORMATS_HTML_JSON); try (CloseableHttpResponse response = myHttpClient.execute(request)) { String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); ourLog.info("Response:\n{}", responseText); @@ -172,19 +194,75 @@ public class FhirPathFilterInterceptorTest { } - private void createPatient() { + public static Stream getBundleParameters() { + return Stream.of( + Arguments.of("Bundle.entry.resource.type", "valueCodeableConcept"), + Arguments.of("Bundle.entry.resource.ofType(Patient).identifier", "valueIdentifier"), + Arguments.of("Bundle.entry.resource.ofType(MedicationAdministration).effective", "valuePeriod"), + Arguments.of("Bundle.entry[0].resource.as(Composition).type", "valueCodeableConcept"), + Arguments.of("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier", "valueIdentifier"), + Arguments.of("Bundle.entry[0].resource.as(Composition).section.entry.resolve().as(MedicationAdministration).effective", "valuePeriod") + ); + } + + @ParameterizedTest + @MethodSource(value = "getBundleParameters") + public void testFilteredResponse_withBundleComposition_returnsResult(final String theFhirPathExpression, final String expectedResult) throws IOException { + IIdType bundle = createBundleDocument(); + + HttpGet request = new HttpGet(bundle.getValue() + "?_fhirpath=" + theFhirPathExpression); + try (CloseableHttpResponse response = myHttpClient.execute(request)) { + String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + ourLog.info("Response:\n{}", responseText); + IBaseResource resource = ourCtx.newJsonParser().parseResource(responseText); + assertTrue(resource instanceof Parameters); + Parameters parameters = (Parameters)resource; + Parameters.ParametersParameterComponent parameterComponent = parameters.getParameter("result"); + assertNotNull(parameterComponent); + assertEquals(2, parameterComponent.getPart().size()); + Parameters.ParametersParameterComponent resultComponent = parameterComponent.getPart().get(1); + assertEquals("result", resultComponent.getName()); + assertThat(responseText, containsString(expectedResult)); + } + + } + + private IIdType createPatient() { Patient p = new Patient(); p.addExtension() - .setUrl("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") - .addExtension() - .setUrl("ombCategory") - .setValue(new Coding("urn:oid:2.16.840.1.113883.6.238", "2106-3", "White")); + .setUrl("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") + .addExtension() + .setUrl("ombCategory") + .setValue(new Coding("urn:oid:2.16.840.1.113883.6.238", "2106-3", "White")); p.setActive(true); p.addIdentifier().setSystem("http://identifiers/1").setValue("value-1"); p.addIdentifier().setSystem("http://identifiers/2").setValue("value-2"); p.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay"); p.addName().setFamily("Simpson").addGiven("Grandpa"); - myPatientId = myClient.create().resource(p).execute().getId().withServerBase(myBaseUrl, "Patient"); + return myClient.create().resource(p).execute().getId().withServerBase(myBaseUrl, "Patient"); } + private IIdType createBundleDocument() { + Patient patient = new Patient(); + patient.setActive(true); + patient.addIdentifier().setSystem("http://identifiers/1").setValue("value-1"); + patient.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay"); + patient = (Patient) myClient.create().resource(patient).execute().getResource(); + + MedicationAdministration medicationAdministration = new MedicationAdministration(); + medicationAdministration.setEffective(new Period().setStartElement(DateTimeType.now())); + medicationAdministration = (MedicationAdministration) myClient.create().resource(medicationAdministration).execute().getResource(); + + 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()); + composition.addSection().addEntry(new Reference(medicationAdministration.getIdElement())); + bundle.addEntry().setResource(patient); + bundle.addEntry().setResource(medicationAdministration); + + return myClient.create().resource(bundle).execute().getId().withServerBase(myBaseUrl, "Bundle"); + } }