Skip to content

Commit

Permalink
Issue #896 - Add checking for MIME-type parameter fhirVersion
Browse files Browse the repository at this point in the history
Signed-off-by: Troy Biesterfeld <tbieste@us.ibm.com>
  • Loading branch information
tbieste committed Jun 9, 2021
1 parent ea0d228 commit cd2c450
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 13 deletions.
4 changes: 2 additions & 2 deletions docs/src/pages/Conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
layout: post
title: Conformance
description: Notes on the Conformance of the IBM FHIR Server
date: 2021-05-19
date: 2021-05-26
permalink: /conformance/
---

Expand All @@ -12,7 +12,7 @@ The IBM FHIR Server aims to be a conformant implementation of the HL7 FHIR speci
## Capability statement
The HL7 FHIR specification defines [an interaction](https://www.hl7.org/fhir/R4/http.html#capabilities) for retrieving a machine-readable description of the server's capabilities via the `[base]/metadata` endpoint. The IBM FHIR Server implements this interaction and generates a `CapabilityStatement` resource based on the current server configuration. While the `CapabilityStatement` resource is ideal for certain uses, this markdown document provides a human-readable summary of important details, with a special focus on limitations of the current implementation and deviations from the specification.

The IBM FHIR Server supports only version 4.0.1 of the specification and ignores the optional MIME-type parameter `fhirVersion`.
The IBM FHIR Server supports only version 4.0.1 of the specification.

## FHIR HTTP API
The HL7 FHIR specification is more than just a data format. It defines an [HTTP API](https://www.hl7.org/fhir/R4/http.html) for creating, reading, updating, deleting, and searching over FHIR resources. The IBM FHIR Server implements the full API for every resource defined in the specification, with the following exceptions:
Expand Down
17 changes: 15 additions & 2 deletions fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
/*
* (C) Copyright IBM Corp. 2019
* (C) Copyright IBM Corp. 2019, 2021
*
* SPDX-License-Identifier: Apache-2.0
*/

package com.ibm.fhir.core;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.core.MediaType;

/**
* This class contains definitions of some non-standard media types.
*/
public class FHIRMediaType extends MediaType {

public final static String SUBTYPE_FHIR_JSON = "fhir+json";
public final static String APPLICATION_FHIR_JSON = "application/" + SUBTYPE_FHIR_JSON;
public final static MediaType APPLICATION_FHIR_JSON_TYPE = new MediaType("application", SUBTYPE_FHIR_JSON);
Expand All @@ -29,6 +35,13 @@ public class FHIRMediaType extends MediaType {
public final static MediaType APPLICATION_FHIR_NDJSON_TYPE = new MediaType("application", SUBTYPE_FHIR_NDJSON);

public final static String SUBTYPE_FHIR_PARQUET = "fhir+parquet";
public static final String APPLICATION_PARQUET = "application/" + SUBTYPE_FHIR_PARQUET;
public static final String APPLICATION_PARQUET = "application/" + SUBTYPE_FHIR_PARQUET;
public final static MediaType APPLICATION_FHIR_PARQUET_TYPE = new MediaType("application", SUBTYPE_FHIR_PARQUET);

// Supported values for the MIME-type parameter fhirVersion.
// https://www.hl7.org/fhir/http.html#version-parameter
// The value of this parameter is the publication and major version number for the specification.
public static final String FHIR_VERSION_PARAMETER = "fhirVersion";
public static final Set<String> SUPPORTED_FHIR_VERSIONS =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("4.0")));
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,38 @@ public void testMetadataAPI_XML() {
assertNotNull(conf.getName());
}

/**
* Verify the 'metadata' API with valid fhirVersion in Accept header.
*/
@Test(groups = { "server-basic" })
public void testMetadataAPI_validFhirVersion() {
WebTarget target = getWebTarget();
MediaType mediaType = new MediaType("application", "fhir+json",
Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "4.0"));
Response response = target.path("metadata").request(mediaType).get();
assertResponse(response, Response.Status.OK.getStatusCode());
assertEquals(mediaType, response.getMediaType());

CapabilityStatement conf = response.readEntity(CapabilityStatement.class);
assertNotNull(conf);
assertNotNull(conf.getFormat());
assertEquals(6, conf.getFormat().size());
assertNotNull(conf.getVersion());
assertNotNull(conf.getName());
}

/**
* Verify the 'metadata' API with invalid fhirVersion in Accept header.
*/
@Test(groups = { "server-basic" })
public void testMetadataAPI_invalidFhirVersion() {
WebTarget target = getWebTarget();
MediaType mediaType = new MediaType("application", "fhir+json",
Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "3.0"));
Response response = target.path("metadata").request(mediaType).get();
assertResponse(response, Response.Status.NOT_ACCEPTABLE.getStatusCode());
}

/**
* Create a Patient, then make sure we can retrieve it.
*/
Expand Down Expand Up @@ -175,6 +207,50 @@ public void testCreatePatient_minimal() throws Exception {
TestUtil.assertResourceEquals(patient, responsePatient);
}

/**
* Create a minimal Patient with valid fhirVersion in Content-Type header, then make sure we can retrieve it.
*/
@Test(groups = { "server-basic" })
public void testCreatePatient_minimal_validFhirVersion() throws Exception {
WebTarget target = getWebTarget();

// Build a new Patient and then call the 'create' API.
Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json");

MediaType mediaType = new MediaType("application", "fhir+json",
Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "4.0"));
Entity<Patient> entity = Entity.entity(patient, mediaType);
Response response = target.path("Patient").request().post(entity, Response.class);
assertResponse(response, Response.Status.CREATED.getStatusCode());

// Get the patient's logical id value.
String patientId = getLocationLogicalId(response);

// Next, call the 'read' API to retrieve the new patient and verify it.
response = target.path("Patient/" + patientId).request(mediaType).get();
assertResponse(response, Response.Status.OK.getStatusCode());
Patient responsePatient = response.readEntity(Patient.class);

TestUtil.assertResourceEquals(patient, responsePatient);
}

/**
* Attempt to create a minimal Patient with invalid fhirVersion in Content-Type header.
*/
@Test( groups = { "server-basic" })
public void testCreatePatient_minimal_invalidFhirVersion() throws Exception {
WebTarget target = getWebTarget();

// Build a new Patient and then call the 'create' API.
Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json");

MediaType mediaType = new MediaType("application", "fhir+json",
Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "3.0"));
Entity<Patient> entity = Entity.entity(patient, mediaType);
Response response = target.path("Patient").request().post(entity, Response.class);
assertResponse(response, Response.Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode());
}

/**
* Create a minimal Patient, then make sure we can retrieve it with varying format
*/
Expand All @@ -193,13 +269,60 @@ public void testCreatePatientMinimalWithFormat() throws Exception {
String patientId = getLocationLogicalId(response);

// Next, call the 'read' API to retrieve the new patient and verify it.
response = target.path("Patient/" + patientId).request(FHIRMediaType.APPLICATION_FHIR_JSON).header("_format", "application/fhir+json").get();
response = target.path("Patient/" + patientId).queryParam("_format", "application/fhir+json").request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
assertResponse(response, Response.Status.OK.getStatusCode());
Patient responsePatient = response.readEntity(Patient.class);

TestUtil.assertResourceEquals(patient, responsePatient);
}

/**
* Create a minimal Patient, then make sure we can retrieve it with varying format with valid FHIR version
*/
@Test(groups = { "server-basic" })
public void testCreatePatientMinimalWithFormat_validFhirVersion() throws Exception {
WebTarget target = getWebTarget();

// Build a new Patient and then call the 'create' API.
Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json");

Entity<Patient> entity = Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON);
Response response = target.path("Patient").request().post(entity, Response.class);
assertResponse(response, Response.Status.CREATED.getStatusCode());

// Get the patient's logical id value.
String patientId = getLocationLogicalId(response);

// Next, call the 'read' API to retrieve the new patient and verify it.
response = target.path("Patient/" + patientId).queryParam("_format", "application/fhir+json;fhirVersion=4.0").request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
assertResponse(response, Response.Status.OK.getStatusCode());
Patient responsePatient = response.readEntity(Patient.class);

TestUtil.assertResourceEquals(patient, responsePatient);
}

/**
* Create a minimal Patient, then attempt to retrieve it with varying format with invalid FHIR version
*/
@Test(groups = { "server-basic" })
public void testCreatePatientMinimalWithFormat_invalidFhirVersion() throws Exception {
WebTarget target = getWebTarget();

// Build a new Patient and then call the 'create' API.
Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json");

Entity<Patient> entity = Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON);
Response response = target.path("Patient").request().post(entity, Response.class);
assertResponse(response, Response.Status.CREATED.getStatusCode());

// Get the patient's logical id value.
String patientId = getLocationLogicalId(response);

// Next, call the 'read' API to attempt to retrieve the new patient
response = target.path("Patient/" + patientId).queryParam("_format", "application/fhir+json;fhirVersion=3.0").request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
assertResponse(response, Response.Status.NOT_ACCEPTABLE.getStatusCode());
}

/**
* Create a minimal Patient, then make sure we can retrieve it.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* (C) Copyright IBM Corp. 2017,2019
* (C) Copyright IBM Corp. 2017, 2021
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -45,18 +45,18 @@ public void testPrettyFormatting() throws Exception {

// Next, call the 'read' API to retrieve the new patient and verify it.
response =
target.queryParam("_pretty", "true").path("Patient/" + patientId)
.request(FHIRMediaType.APPLICATION_FHIR_JSON).header("_format", "application/fhir+json").get();
target.queryParam("_pretty", "true").queryParam("_format", "application/fhir+json").path("Patient/" + patientId)
.request(FHIRMediaType.APPLICATION_FHIR_JSON).get();
assertResponse(response, Response.Status.OK.getStatusCode());

String prettyOutput = response.readEntity(String.class);

response =
target.queryParam("_pretty", "false").path("Patient/" + patientId)
.request(FHIRMediaType.APPLICATION_FHIR_JSON).header("_format", "application/fhir+json").get();
target.queryParam("_pretty", "false").queryParam("_format", "application/fhir+json").path("Patient/" + patientId)
.request(FHIRMediaType.APPLICATION_FHIR_JSON).get();

String notPrettyOutput = response.readEntity(String.class);

assertNotEquals(prettyOutput, notPrettyOutput);
assertFalse(notPrettyOutput.contains("\n"));
assertTrue(prettyOutput.contains("\n"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;

Expand All @@ -32,6 +33,7 @@
import com.ibm.fhir.config.FHIRConfiguration;
import com.ibm.fhir.config.FHIRRequestContext;
import com.ibm.fhir.config.PropertyGroup;
import com.ibm.fhir.core.FHIRMediaType;
import com.ibm.fhir.core.HTTPHandlingPreference;
import com.ibm.fhir.core.HTTPReturnPreference;
import com.ibm.fhir.exception.FHIRException;
Expand Down Expand Up @@ -115,6 +117,8 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F
String encodedRequestDescription = Encode.forHtml(requestDescription.toString());
log.info("Received request: " + encodedRequestDescription);

int statusOnException = HttpServletResponse.SC_BAD_REQUEST;

try {
// Checks for Valid Tenant Configuration
checkValidTenantConfiguration(tenantId);
Expand All @@ -138,16 +142,30 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F
Map<String, List<String>> requestHeaders = extractRequestHeaders(request);
context.setHttpHeaders(requestHeaders);

// Check the FHIR version parameter
// 415 Unsupported Media Type is the appropriate response when the client posts a format that is not supported to the server.
// 406 Not Acceptable is the appropriate response when the Accept header requests a format that the server does not support.
String errorMsg = checkFhirVersionParameter(HttpHeaders.CONTENT_TYPE, requestHeaders);
if (errorMsg != null) {
statusOnException = HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE;
throw new FHIRException(errorMsg);
}
errorMsg = checkFhirVersionParameter(HttpHeaders.ACCEPT, requestHeaders);
if (errorMsg != null) {
statusOnException = HttpServletResponse.SC_NOT_ACCEPTABLE;
throw new FHIRException(errorMsg);
}

// Pass the request through to the next filter in the chain.
chain.doFilter(request, response);
} catch (Exception e) {
log.log(Level.INFO, "Error while setting request context or processing request", e);

OperationOutcome outcome = FHIRUtil.buildOperationOutcome(e, IssueType.INVALID, IssueSeverity.FATAL, false);

response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setStatus(statusOnException);

Format format = chooseResponseFormat(request.getHeader("Accept"));
Format format = chooseResponseFormat(request.getHeader(HttpHeaders.ACCEPT));
switch (format) {
case XML:
response.setContentType(com.ibm.fhir.core.FHIRMediaType.APPLICATION_FHIR_XML);
Expand Down Expand Up @@ -261,6 +279,30 @@ private HTTPReturnPreference computeReturnPref(ServletRequest request, HTTPHandl
return returnPref;
}

/**
* Checks if the FHIR version parameter in the specified HTTP header is valid.
* @param headerName the name of the header
* @param requestHeaders the headers
* @return the error message if FHIR version parameter in not valid, otherwise null
*/
private String checkFhirVersionParameter(String headerName, Map<String, List<String>> requestHeaders) throws FHIRException {
for (String headerValue : requestHeaders.getOrDefault(headerName, Collections.emptyList())) {
Map<String, String> parameters = MediaType.valueOf(headerValue).getParameters();
if (parameters != null) {
for (Map.Entry<String, String> parameter : parameters.entrySet()) {
if (FHIRMediaType.FHIR_VERSION_PARAMETER.equalsIgnoreCase(parameter.getKey())) {
String fhirVersion = parameter.getValue();
if (fhirVersion != null && !FHIRMediaType.SUPPORTED_FHIR_VERSIONS.contains(fhirVersion)) {
return "Invalid '" + FHIRMediaType.FHIR_VERSION_PARAMETER + "' parameter value in '" + headerName
+ "' header; the following FHIR versions are supported: " + FHIRMediaType.SUPPORTED_FHIR_VERSIONS;
}
}
}
}
}
return null;
}

private Format chooseResponseFormat(String acceptableContentTypes) {
if (acceptableContentTypes.contains(com.ibm.fhir.core.FHIRMediaType.APPLICATION_FHIR_JSON) ||
acceptableContentTypes.contains(MediaType.APPLICATION_JSON)) {
Expand Down

0 comments on commit cd2c450

Please sign in to comment.