diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index 499e0cab5f7..ad6c80c8ffe 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -2082,6 +2082,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/ifNoneMatchReturnsNotModified`|boolean|When If-None-Match is specified, overrides the standard return status "412 Precondition Failed" to be "304 Not Modified". Useful in transaction bundles for clients not wanting the bundle to fail when a conflict is found.| |`fhirServer/core/capabilitiesUrl`|string|The URL that is embedded in the default Capabilities statement| |`fhirServer/core/externalBaseUrl`|string|The base URL that is embedded in the Search bundle response, as of version 4.9.0. Note that the base URL must not include a path segment that matches any FHIR resource type name (case-sensitive). For example, "https://example.com" or "https://example.com/my/patient/api" are fine, but "https://example.com/my/Patient/api" is not.| +|`fhirServer/core/defaultFhirVersion`|string|The implicit value to use for the MIME-type fhirVersion parameter on incoming Accept and Content-Type headers when the client has not passed an explicit value.| |`fhirServer/core/useImplicitTypeScopingForWholeSystemInteractions`|boolean|Whether to apply implicit resource type scoping for whole-system search and whole-system history interactions where no `_type` values were passed. Only set to false if you are certain that there are no instances of unsupported resource types in the database.| |`fhirServer/validation/failFast`|boolean|Indicates whether validation should fail fast on create and update interactions| |`fhirServer/term/capabilitiesUrl`|string|The URL that is embedded in the Terminology Capabilities statement using `mode=terminology`| @@ -2266,6 +2267,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/capabilitiesUrl`|null| |`fhirServer/core/externalBaseUrl`|null| |`fhirServer/core/ifNoneMatchReturnsNotModified`|false| +|`fhirServer/core/defaultFhirVersion`|null| |`fhirServer/core/useImplicitTypeScopingForWholeSystemInteractions`|true| |`fhirServer/validation/failFast`|false| |`fhirServer/term/capabilitiesUrl`|null| @@ -2419,6 +2421,7 @@ must restart the server for that change to take effect. |`fhirServer/core/maxPageIncludeCount`|Y|Y| |`fhirServer/core/capabilitiesUrl`|Y|Y| |`fhirServer/core/externalBaseUrl`|Y|Y| +|`fhirServer/core/defaultFhirVersion`|Y|Y| |`fhirServer/core/useImplicitTypeScopingForWholeSystemInteractions`|Y|Y| |`fhirServer/validation/failFast`|Y|Y| |`fhirServer/term/cachingDisabled`|N|N| diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java index 565ae76c584..fbe72d208a3 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java @@ -43,6 +43,7 @@ public class FHIRConfiguration { public static final String PROPERTY_MAX_PAGE_SIZE = "fhirServer/core/maxPageSize"; public static final String PROPERTY_MAX_PAGE_INCLUDE_COUNT = "fhirServer/core/maxPageIncludeCount"; public static final String PROPERTY_CAPABILITIES_URL = "fhirServer/core/capabilitiesUrl"; + public static final String PROPERTY_DEFAULT_FHIR_VERSION = "fhirServer/core/defaultFhirVersion"; // Migration properties public static final String PROPERTY_WHOLE_SYSTEM_TYPE_SCOPING = "fhirServer/core/useImplicitTypeScopingForWholeSystemInteractions"; diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/CapabilitiesVersionTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/CapabilitiesVersionTest.java new file mode 100644 index 00000000000..f320e55874e --- /dev/null +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/CapabilitiesVersionTest.java @@ -0,0 +1,124 @@ +/* + * (C) Copyright IBM Corp. 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.server.test; + +import static com.ibm.fhir.core.FHIRMediaType.FHIR_VERSION_PARAMETER; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; +import static org.testng.AssertJUnit.assertNotNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.ibm.fhir.core.FHIRMediaType; +import com.ibm.fhir.model.resource.CapabilityStatement; +import com.ibm.fhir.model.resource.CapabilityStatement.Rest; +import com.ibm.fhir.model.resource.CapabilityStatement.Rest.Resource; +import com.ibm.fhir.model.type.code.ResourceType; +import com.ibm.fhir.path.exception.FHIRPathException; +import com.ibm.fhir.validation.exception.FHIRValidationException; + +public class CapabilitiesVersionTest extends FHIRServerTestBase { + private static final Set R4B_ONLY_RESOURCES = new HashSet<>(); + { + R4B_ONLY_RESOURCES.add(ResourceType.Value.ADMINISTRABLE_PRODUCT_DEFINITION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.CITATION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.CLINICAL_USE_DEFINITION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.EVIDENCE_REPORT); + R4B_ONLY_RESOURCES.add(ResourceType.Value.INGREDIENT); + R4B_ONLY_RESOURCES.add(ResourceType.Value.MANUFACTURED_ITEM_DEFINITION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.MEDICINAL_PRODUCT_DEFINITION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.NUTRITION_PRODUCT); + R4B_ONLY_RESOURCES.add(ResourceType.Value.PACKAGED_PRODUCT_DEFINITION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.REGULATED_AUTHORIZATION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.SUBSCRIPTION_STATUS); + R4B_ONLY_RESOURCES.add(ResourceType.Value.SUBSCRIPTION_TOPIC); + R4B_ONLY_RESOURCES.add(ResourceType.Value.SUBSTANCE_DEFINITION); + // The following resource types existed in R4, but have breaking changes in R4B. + // Because we only support the R4B version, we don't want to advertise these in our 4.0.1 statement. + R4B_ONLY_RESOURCES.add(ResourceType.Value.DEVICE_DEFINITION); + R4B_ONLY_RESOURCES.add(ResourceType.Value.EVIDENCE); + R4B_ONLY_RESOURCES.add(ResourceType.Value.EVIDENCE_VARIABLE); + // TODO: make final decision on whether to lump these in with the breaking resources + // R4B_ONLY_RESOURCES.add(ResourceType.Value.PLAN_DEFINITION); + // R4B_ONLY_RESOURCES.add(ResourceType.Value.ACTIVITY_DEFINITION); + } + + /** + * Verify the 'metadata' API. + */ + @Test(dataProvider = "dataMethod") + public void testWithTenantAndFHIRVersion(String tenant, String fhirVersion) throws FHIRPathException, FHIRValidationException { + WebTarget target = getWebTarget(); + Map fhirVersionParameterMap = (fhirVersion == null) ? null : Collections.singletonMap(FHIR_VERSION_PARAMETER, fhirVersion); + MediaType mediaType = new MediaType("application", FHIRMediaType.SUBTYPE_FHIR_JSON, fhirVersionParameterMap); + + Response response = target.path("metadata") + .request(mediaType) + .header("X-FHIR-TENANT-ID", tenant) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + + CapabilityStatement conf = response.readEntity(CapabilityStatement.class); + assertNotNull(conf); + assertNotNull(conf.getFhirVersion()); + if (fhirVersion != null) { + assertTrue(conf.getFhirVersion().getValue().startsWith(fhirVersion)); + } + + switch (conf.getFhirVersion().getValueAsEnum()) { + case VERSION_4_0_1: + // verify it has no "R4B-only" resource types + for (Rest rest : conf.getRest()) { + for (Resource resource : rest.getResource()) { + assertFalse(R4B_ONLY_RESOURCES.contains(resource.getType().getValueAsEnum()), + "unexpected resource type: " + resource.getType().getValue()); + } + } + break; + case VERSION_4_3_0_CIBUILD: + // nothing to verify at the moment + break; + default: + fail("unexpected fhirVersion: " + conf.getFhirVersion().getValue()); + } + } + + /** + * tenant, fhirVersion + */ + @DataProvider + public static Object[][] dataMethod() { + String[] tenants = new String[] { + "default", // defaultFhirVersion=4.0 + "tenant1", // defaultFhirVersion=4.3 + "tenant2" // no defaultFhirVersion configured + }; + String[] versions = new String[] {null, "4.0", "4.3"}; + + // compute the cartesian product + Object[][] inputs = new Object[tenants.length * versions.length][2]; + int i = 0; + for (String tenant : tenants) { + for (String version : versions) { + inputs[i++] = new Object[] {tenant, version}; + } + } + + return inputs; + } +} diff --git a/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json b/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json index e08fe80beeb..ee8777b8548 100644 --- a/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json +++ b/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json @@ -8,7 +8,8 @@ "checkReferenceTypes": true, "conditionalDeleteMaxNumber": 10, "serverRegistryResourceProviderEnabled": true, - "disabledOperations": "" + "disabledOperations": "", + "defaultFhirVersion": "4.0" }, "search": { "useStoredCompartmentParam": true diff --git a/fhir-server-webapp/src/test/liberty/config/config/tenant1/fhir-server-config.json b/fhir-server-webapp/src/test/liberty/config/config/tenant1/fhir-server-config.json index 62302ae75ee..f8c4bfdaf71 100644 --- a/fhir-server-webapp/src/test/liberty/config/config/tenant1/fhir-server-config.json +++ b/fhir-server-webapp/src/test/liberty/config/config/tenant1/fhir-server-config.json @@ -39,7 +39,8 @@ "defaultPageSize": 11, "maxPageSize": 1001, "maxPageIncludeCount": 1000, - "externalBaseUrl": "https://chocolate.fudge" + "externalBaseUrl": "https://chocolate.fudge", + "defaultFhirVersion": "4.3" }, "persistence": { "datasources": { diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/FHIRApplication.java b/fhir-server/src/main/java/com/ibm/fhir/server/FHIRApplication.java index 0573243e6ab..5f507d737f3 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/FHIRApplication.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/FHIRApplication.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2016, 2020 + * (C) Copyright IBM Corp. 2016, 2022 * * SPDX-License-Identifier: Apache-2.0 */ @@ -30,6 +30,7 @@ import com.ibm.fhir.server.resources.Update; import com.ibm.fhir.server.resources.VRead; import com.ibm.fhir.server.resources.WellKnown; +import com.ibm.fhir.server.resources.filters.FHIRVersionRequestFilter; import com.ibm.fhir.server.resources.filters.OriginalRequestFilter; public class FHIRApplication extends Application { @@ -64,6 +65,7 @@ public Set> getClasses() { classes.add(Search.class); classes.add(Update.class); classes.add(VRead.class); + classes.add(FHIRVersionRequestFilter.class); classes.add(OriginalRequestFilter.class); if (FHIRConfigHelper.getBooleanProperty(FHIRConfiguration.PROPERTY_SECURITY_OAUTH_SMART_ENABLED, false)) { classes.add(WellKnown.class); diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java b/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java index 9cff50b6c57..24c8d4d520a 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java @@ -38,7 +38,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; @@ -47,7 +46,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.CacheControl; -import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -155,9 +153,6 @@ public class Capabilities extends FHIRResource { private static final String CAPABILITY_STATEMENT_CACHE_NAME = "com.ibm.fhir.server.resources.Capabilities.statementCache"; - @Context - protected HttpServletRequest httpServletRequest; - // Constructor public Capabilities() throws Exception { super(); @@ -175,8 +170,6 @@ public Response capabilities(@QueryParam("mode") @DefaultValue("full") String mo throw new IllegalArgumentException("Invalid mode parameter: must be one of [full, normative, terminology]"); } - FHIRVersion fhirVersion = getFhirVersion(accept); - // Defaults to 60 minutes (or what's in the fhirConfig) int cacheTimeout = FHIRConfigHelper.getIntProperty(PROPERTY_CAPABILITY_STATEMENT_CACHE, 60); Configuration configuration = Configuration.of(Duration.of(cacheTimeout, ChronoUnit.MINUTES)); @@ -184,6 +177,7 @@ public Response capabilities(@QueryParam("mode") @DefaultValue("full") String mo Map cacheAsMap = CacheManager.getCacheAsMap(CAPABILITY_STATEMENT_CACHE_NAME, configuration); CacheManager.reportCacheStats(log, CAPABILITY_STATEMENT_CACHE_NAME); + FHIRVersion fhirVersion = getFhirVersion(); String cacheKey = mode + "-" + fhirVersion.getValue(); Resource capabilityStatement = cacheAsMap.computeIfAbsent(cacheKey, k -> computeCapabilityStatement(mode, fhirVersion)); @@ -211,25 +205,6 @@ public Response capabilities(@QueryParam("mode") @DefaultValue("full") String mo } } - /** - * Which FHIRVersion to use for the generated CapabilityStatement - * - * @param acceptHeaderValue - * @return 4.3.0 if the client is asking for it, otherwise 4.0.1 - */ - private FHIRVersion getFhirVersion(String acceptHeaderValue) { - if (acceptHeaderValue != null && !acceptHeaderValue.isEmpty()) { - for (String headerValueElement : acceptHeaderValue.split(",")) { - String requestedVersion = MediaType.valueOf(headerValueElement).getParameters().get(FHIRMediaType.FHIR_VERSION_PARAMETER); - if ("4.3".equals(requestedVersion) || "4.3.0".equals(requestedVersion)) { - // TODO: remove _CIBUILD after generating from the published 4.3.0 artifacts - return FHIRVersion.VERSION_4_3_0_CIBUILD; - } - } - } - return FHIRVersion.VERSION_4_0_1; - } - private boolean isValidMode(String mode) { return "full".equals(mode) || "normative".equals(mode) || "terminology".equals(mode); } diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/resources/FHIRResource.java b/fhir-server/src/main/java/com/ibm/fhir/server/resources/FHIRResource.java index 9ab811194e0..30c003055e0 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/resources/FHIRResource.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/resources/FHIRResource.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2016, 2021 + * (C) Copyright IBM Corp. 2016, 2022 * * SPDX-License-Identifier: Apache-2.0 */ @@ -42,6 +42,7 @@ import com.ibm.fhir.config.FHIRRequestContext; import com.ibm.fhir.config.PropertyGroup; import com.ibm.fhir.core.FHIRConstants; +import com.ibm.fhir.core.FHIRMediaType; import com.ibm.fhir.exception.FHIROperationException; import com.ibm.fhir.model.format.Format; import com.ibm.fhir.model.generator.FHIRGenerator; @@ -52,6 +53,7 @@ import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.CodeableConcept; import com.ibm.fhir.model.type.Extension; +import com.ibm.fhir.model.type.code.FHIRVersion; import com.ibm.fhir.model.type.code.IssueSeverity; import com.ibm.fhir.model.type.code.IssueType; import com.ibm.fhir.model.util.FHIRUtil; @@ -62,6 +64,7 @@ import com.ibm.fhir.persistence.helper.PersistenceHelper; import com.ibm.fhir.server.exception.FHIRRestBundledRequestException; import com.ibm.fhir.server.listener.FHIRServletContextListener; +import com.ibm.fhir.server.resources.filters.FHIRVersionRequestFilter; import net.jcip.annotations.NotThreadSafe; @@ -502,4 +505,18 @@ protected FHIROperationException buildUnsupportedResourceTypeException(String re .build(); return new FHIROperationException(msg).withIssue(issue); } + + /** + * The FHIRVersion to use for the current request + * + * @return the corresponding FHIRVersion for the com.ibm.fhir.server.fhirVersion request context attribute + */ + protected FHIRVersion getFhirVersion() { + String fhirVersionString = (String) httpServletRequest.getAttribute(FHIRVersionRequestFilter.FHIR_VERSION_PROP); + if (FHIRMediaType.VERSION_43.equals(fhirVersionString)) { + return FHIRVersion.VERSION_4_3_0_CIBUILD; + } else { + return FHIRVersion.VERSION_4_0_1; + } + } } diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/resources/filters/FHIRVersionRequestFilter.java b/fhir-server/src/main/java/com/ibm/fhir/server/resources/filters/FHIRVersionRequestFilter.java new file mode 100644 index 00000000000..1df71b920e7 --- /dev/null +++ b/fhir-server/src/main/java/com/ibm/fhir/server/resources/filters/FHIRVersionRequestFilter.java @@ -0,0 +1,59 @@ +/* + * (C) Copyright IBM Corp. 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ + + package com.ibm.fhir.server.resources.filters; + +import static com.ibm.fhir.core.FHIRMediaType.VERSION_40; +import static com.ibm.fhir.core.FHIRMediaType.VERSION_43; + +import java.io.IOException; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.MediaType; + +import com.ibm.fhir.config.FHIRConfigHelper; +import com.ibm.fhir.config.FHIRConfiguration; +import com.ibm.fhir.core.FHIRMediaType; + +public class FHIRVersionRequestFilter implements ContainerRequestFilter { + public static final String FHIR_VERSION_PROP = "com.ibm.fhir.server.fhirVersion"; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + /* + * This method will look through the MediaTypes constructed by JAX-RS from the incoming "Accept" header + * and add the most preferred value to the request context under the FHIR_VERSION_PROP name using the following + * order of preference: + * + * 1. "4.3" from the acceptableMediaTypes + * 2. "4.0" from the acceptableMediaTypes + * 3. whatever is configured in the fhirServer/core/defaultFhirVersion config property + * 4. "4.0" + */ + String fhirVersion = null; + for (MediaType mediaType : requestContext.getAcceptableMediaTypes()) { + if (mediaType.getParameters() != null) { + String fhirVersionParam = mediaType.getParameters().get(FHIRMediaType.FHIR_VERSION_PARAMETER); + if (fhirVersionParam != null) { + // "startsWith" to cover the x.y.x cases which are technically invalid, but close enough + if (fhirVersionParam.startsWith(VERSION_43)) { + // one of the acceptable media types was our "actual" fhir version, so use that and stop looking + fhirVersion = VERSION_43; + break; + } else if (fhirVersionParam.startsWith(VERSION_40)) { + // set the fhirVersion parameter but keep looking in case our "actual" version is also acceptable + fhirVersion = fhirVersionParam; + } + } + } + } + if (fhirVersion == null) { + fhirVersion = FHIRConfigHelper.getStringProperty(FHIRConfiguration.PROPERTY_DEFAULT_FHIR_VERSION, FHIRMediaType.VERSION_40); + } + requestContext.setProperty(FHIR_VERSION_PROP, fhirVersion); + } +} \ No newline at end of file