Skip to content

Commit

Permalink
issue #3242 - introduce defaultFhirVersion config property
Browse files Browse the repository at this point in the history
This changeset adds a JAX-RS RequestFilter that sets a requestContext
property, "com.ibm.fhir.server.fhirVersion" with the following order of
precedence:
   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"

Additionally, I updated the Capabilities resource to use the fhirVersion
from the request context instead of processing the Accept header itself.

Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com>
  • Loading branch information
lmsurpre committed Feb 5, 2022
1 parent a551ab3 commit bdde956
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 30 deletions.
3 changes: 3 additions & 0 deletions docs/src/pages/guides/FHIRServerUsersGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`|
Expand Down Expand Up @@ -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|
Expand Down Expand Up @@ -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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ResourceType.Value> 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<String,String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"checkReferenceTypes": true,
"conditionalDeleteMaxNumber": 10,
"serverRegistryResourceProviderEnabled": true,
"disabledOperations": ""
"disabledOperations": "",
"defaultFhirVersion": "4.0"
},
"search": {
"useStoredCompartmentParam": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"defaultPageSize": 11,
"maxPageSize": 1001,
"maxPageIncludeCount": 1000,
"externalBaseUrl": "https://chocolate.fudge"
"externalBaseUrl": "https://chocolate.fudge",
"defaultFhirVersion": "4.3"
},
"persistence": {
"datasources": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2016, 2020
* (C) Copyright IBM Corp. 2016, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -64,6 +65,7 @@ public Set<Class<?>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -175,15 +170,14 @@ 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));

Map<String, Resource> 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));

Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2016, 2021
* (C) Copyright IBM Corp. 2016, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit bdde956

Please sign in to comment.