An experimental interceptor called VersionedApiConverterInterceptor has been added, which automaticaly converts response payloads to a client-specified version according to transforms built into FHIR.

This commit is contained in:
James Agnew 2018-01-29 12:10:05 -06:00
parent a89c8d50c5
commit e52f582769
13 changed files with 412 additions and 348 deletions

View File

@ -42,14 +42,12 @@ public enum FhirVersionEnum {
DSTU3("org.hl7.fhir.dstu3.hapi.ctx.FhirDstu3", null, true, new Dstu3Version()), DSTU3("org.hl7.fhir.dstu3.hapi.ctx.FhirDstu3", null, true, new Dstu3Version()),
R4("org.hl7.fhir.r4.hapi.ctx.FhirR4", null, true, new R4Version()), R4("org.hl7.fhir.r4.hapi.ctx.FhirR4", null, true, new R4Version()),;
;
private final FhirVersionEnum myEquivalent; private final FhirVersionEnum myEquivalent;
private final boolean myIsRi; private final boolean myIsRi;
private volatile Boolean myPresentOnClasspath;
private final String myVersionClass; private final String myVersionClass;
private volatile Boolean myPresentOnClasspath;
private volatile IFhirVersion myVersionImplementation; private volatile IFhirVersion myVersionImplementation;
private String myFhirVersionString; private String myFhirVersionString;
@ -82,21 +80,6 @@ public enum FhirVersionEnum {
return ordinal() >= theVersion.ordinal(); return ordinal() >= theVersion.ordinal();
} }
/**
* Returns the {@link FhirVersionEnum} which corresponds to a specific version of
* FHIR. Partial version strings (e.g. "3.0") are acceptable.
*
* @return Returns null if no version exists matching the given string
*/
public static FhirVersionEnum forVersionString(String theVersionString) {
for (FhirVersionEnum next : values()) {
if (next.getFhirVersionString().startsWith(theVersionString)) {
return next;
}
}
return null;
}
public boolean isEquivalentTo(FhirVersionEnum theVersion) { public boolean isEquivalentTo(FhirVersionEnum theVersion) {
if (this.equals(theVersion)) { if (this.equals(theVersion)) {
return true; return true;
@ -139,15 +122,50 @@ public enum FhirVersionEnum {
return myIsRi; return myIsRi;
} }
public FhirContext newContext() {
switch (this) {
case DSTU2:
return FhirContext.forDstu2();
case DSTU2_HL7ORG:
return FhirContext.forDstu2Hl7Org();
case DSTU2_1:
return FhirContext.forDstu2_1();
case DSTU3:
return FhirContext.forDstu3();
case R4:
return FhirContext.forR4();
}
throw new IllegalStateException("Unknown version: " + this); // should not happen
}
/**
* Returns the {@link FhirVersionEnum} which corresponds to a specific version of
* FHIR. Partial version strings (e.g. "3.0") are acceptable.
*
* @return Returns null if no version exists matching the given string
*/
public static FhirVersionEnum forVersionString(String theVersionString) {
for (FhirVersionEnum next : values()) {
if (next.getFhirVersionString().startsWith(theVersionString)) {
return next;
}
}
return null;
}
private interface IVersionProvider {
String provideVersion();
}
private static class Version implements IVersionProvider { private static class Version implements IVersionProvider {
private String myVersion;
public Version(String theVersion) { public Version(String theVersion) {
super(); super();
myVersion = theVersion; myVersion = theVersion;
} }
private String myVersion;
@Override @Override
public String provideVersion() { public String provideVersion() {
return myVersion; return myVersion;
@ -155,17 +173,14 @@ public enum FhirVersionEnum {
} }
private interface IVersionProvider {
String provideVersion();
}
/** /**
* This class attempts to read the FHIR version from the actual model * This class attempts to read the FHIR version from the actual model
* classes in order to supply an accurate version string even over time * classes in order to supply an accurate version string even over time
*
*/ */
private static class Dstu3Version implements IVersionProvider { private static class Dstu3Version implements IVersionProvider {
private String myVersion;
public Dstu3Version() { public Dstu3Version() {
try { try {
Class<?> c = Class.forName("org.hl7.fhir.dstu3.model.Constants"); Class<?> c = Class.forName("org.hl7.fhir.dstu3.model.Constants");
@ -175,8 +190,6 @@ public enum FhirVersionEnum {
} }
} }
private String myVersion;
@Override @Override
public String provideVersion() { public String provideVersion() {
return myVersion; return myVersion;
@ -186,6 +199,8 @@ public enum FhirVersionEnum {
private static class R4Version implements IVersionProvider { private static class R4Version implements IVersionProvider {
private String myVersion;
public R4Version() { public R4Version() {
try { try {
Class<?> c = Class.forName("org.hl7.fhir.r4.model.Constants"); Class<?> c = Class.forName("org.hl7.fhir.r4.model.Constants");
@ -195,8 +210,6 @@ public enum FhirVersionEnum {
} }
} }
private String myVersion;
@Override @Override
public String provideVersion() { public String provideVersion() {
return myVersion; return myVersion;

View File

@ -32,6 +32,12 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId> <artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
@ -70,6 +76,11 @@
</dependency> </dependency>
<!-- Testing --> <!-- Testing -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId> <artifactId>hapi-fhir-client</artifactId>

View File

@ -1,39 +1,68 @@
package ca.uhn.hapi.converters.server; package ca.uhn.hapi.converters.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
import org.hl7.fhir.convertors.VersionConvertor_30_40; import org.hl7.fhir.convertors.*;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* <b>This is an experimental interceptor! Use with caution as
* behaviour may change or be removed in a future version of
* FHIR.</b>
* <p>
* This interceptor partially implements the proposed
* Versioned API features.
* </p>
*/
public class VersionedApiConverterInterceptor extends InterceptorAdapter { public class VersionedApiConverterInterceptor extends InterceptorAdapter {
private VersionConvertor_30_40 myVersionConvertor_30_40 = new VersionConvertor_30_40(); private final FhirContext myCtxDstu2;
private final FhirContext myCtxDstu2Hl7Org;
private VersionConvertor_30_40 myVersionConvertor_30_40;
private VersionConvertor_10_40 myVersionConvertor_10_40;
private VersionConvertor_10_30 myVersionConvertor_10_30;
public VersionedApiConverterInterceptor() {
myVersionConvertor_30_40 = new VersionConvertor_30_40();
VersionConvertorAdvisor40 advisor40 = new NullVersionConverterAdvisor40();
myVersionConvertor_10_40 = new VersionConvertor_10_40(advisor40);
VersionConvertorAdvisor30 advisor30 = new NullVersionConverterAdvisor30();
myVersionConvertor_10_30 = new VersionConvertor_10_30(advisor30);
myCtxDstu2 = FhirContext.forDstu2();
myCtxDstu2Hl7Org = FhirContext.forDstu2Hl7Org();
}
@Override @Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { public boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException {
String accept = defaultString(theServletRequest.getHeader(Constants.HEADER_ACCEPT)); String[] formatParams = theRequestDetails.getParameters().get(Constants.PARAM_FORMAT);
String accept = null;
if (formatParams != null && formatParams.length > 0) {
accept = formatParams[0];
}
if (isBlank(accept)) {
accept = defaultString(theServletRequest.getHeader(Constants.HEADER_ACCEPT));
}
StringTokenizer tok = new StringTokenizer(accept, ";"); StringTokenizer tok = new StringTokenizer(accept, ";");
String wantVersionString = null; String wantVersionString = null;
while (tok.hasMoreTokens()) { while (tok.hasMoreTokens()) {
String next = tok.nextToken().trim(); String next = tok.nextToken().trim();
if (next.startsWith("fhir-version=")) { if (next.startsWith("fhirVersion=")) {
wantVersionString = next.substring("fhir-version=".length()).trim(); wantVersionString = next.substring("fhirVersion=".length()).trim();
break; break;
} }
} }
@ -42,31 +71,48 @@ public class VersionedApiConverterInterceptor extends InterceptorAdapter {
if (isNotBlank(wantVersionString)) { if (isNotBlank(wantVersionString)) {
wantVersion = FhirVersionEnum.forVersionString(wantVersionString); wantVersion = FhirVersionEnum.forVersionString(wantVersionString);
} }
FhirVersionEnum haveVersion = theResponseObject.getStructureFhirVersionEnum();
IBaseResource responseResource = theResponseDetails.getResponseResource();
FhirVersionEnum haveVersion = responseResource.getStructureFhirVersionEnum();
IBaseResource converted = null; IBaseResource converted = null;
try { try {
if (wantVersion == FhirVersionEnum.R4 && haveVersion == FhirVersionEnum.DSTU3) { if (wantVersion == FhirVersionEnum.R4 && haveVersion == FhirVersionEnum.DSTU3) {
converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.dstu3.model.Resource) theResponseObject); converted = myVersionConvertor_30_40.convertResource(toDstu3(responseResource));
} else if (wantVersion == FhirVersionEnum.DSTU3 && haveVersion == FhirVersionEnum.R4) { } else if (wantVersion == FhirVersionEnum.DSTU3 && haveVersion == FhirVersionEnum.R4) {
converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.r4.model.Resource) theResponseObject); converted = myVersionConvertor_30_40.convertResource(toR4(responseResource));
} else if (wantVersion == FhirVersionEnum.DSTU3 && haveVersion == FhirVersionEnum.R4) { } else if (wantVersion == FhirVersionEnum.DSTU2 && haveVersion == FhirVersionEnum.R4) {
converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.r4.model.Resource) theResponseObject); converted = myVersionConvertor_10_40.convertResource(toR4(responseResource));
} else if (wantVersion == FhirVersionEnum.R4 && haveVersion == FhirVersionEnum.DSTU2) {
converted = myVersionConvertor_10_40.convertResource(toDstu2(responseResource));
} else if (wantVersion == FhirVersionEnum.DSTU2 && haveVersion == FhirVersionEnum.DSTU3) {
converted = myVersionConvertor_10_30.convertResource(toDstu3(responseResource));
} else if (wantVersion == FhirVersionEnum.DSTU3 && haveVersion == FhirVersionEnum.DSTU2) {
converted = myVersionConvertor_10_30.convertResource(toDstu2(responseResource));
} }
} catch (FHIRException e) { } catch (FHIRException e) {
throw new InternalErrorException(e); throw new InternalErrorException(e);
} }
if (converted != null) { if (converted != null) {
Set<SummaryEnum> objects = Collections.emptySet(); theResponseDetails.setResponseResource(converted);
try {
RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), converted, objects, 200, "OK", false, false, theRequestDetails, null, null);
return false;
} catch (IOException e) {
throw new InternalErrorException(e);
}
} }
return true; return true;
} }
private org.hl7.fhir.instance.model.Resource toDstu2(IBaseResource theResponseResource) {
if (theResponseResource instanceof IResource) {
return (org.hl7.fhir.instance.model.Resource) myCtxDstu2Hl7Org.newJsonParser().parseResource(myCtxDstu2.newJsonParser().encodeResourceToString(theResponseResource));
}
return (org.hl7.fhir.instance.model.Resource) theResponseResource;
}
private Resource toDstu3(IBaseResource theResponseResource) {
return (Resource) theResponseResource;
}
private org.hl7.fhir.r4.model.Resource toR4(IBaseResource theResponseResource) {
return (org.hl7.fhir.r4.model.Resource) theResponseResource;
}
} }

View File

@ -0,0 +1,56 @@
package org.hl7.fhir.convertors;
/*
* #%L
* HAPI FHIR - Converter
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* 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%
*/
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.Resource;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.ValueSet;
public class NullVersionConverterAdvisor40 implements VersionConvertorAdvisor40 {
@Override
public Resource convertR2(org.hl7.fhir.r4.model.Resource resource) throws FHIRException {
return null;
}
@Override
public org.hl7.fhir.dstu3.model.Resource convertR3(org.hl7.fhir.r4.model.Resource resource) throws FHIRException {
return null;
}
@Override
public CodeSystem getCodeSystem(ValueSet theSrc) {
return null;
}
@Override
public void handleCodeSystem(CodeSystem theTgtcs, ValueSet theSource) {
//nothing
}
@Override
public boolean ignoreEntry(BundleEntryComponent theSrc) {
return false;
}
}

View File

@ -1,276 +0,0 @@
package ca.uhn.hapi.converters.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.HumanName;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
public class SearchDstu3Test {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static TokenAndListParam ourIdentifiers;
private static String ourLastMethod;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchDstu3Test.class);
private static int ourPort;
private static Server ourServer;
@Before
public void before() {
ourLastMethod = null;
ourIdentifiers = null;
}
@Test
public void testSearchNormal() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar");
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("search", ourLastMethod);
assertEquals("foo", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem());
assertEquals("bar", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
public void testSearchWithInvalidChain() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier.chain=foo%7Cbar");
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(400, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) ourCtx.newJsonParser().parseResource(responseContent);
assertEquals(
"Invalid search parameter \"identifier.chain\". Parameter contains a chain (.chain) and chains are not supported for this parameter (chaining is only allowed on reference parameters)",
oo.getIssueFirstRep().getDiagnostics());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
public void testPagingPreservesEncodingJson() throws Exception {
HttpGet httpGet;
String linkNext;
Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=json");
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
}
@Test
public void testPagingPreservesEncodingApplicationJsonFhir() throws Exception {
HttpGet httpGet;
String linkNext;
Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=" + Constants.CT_FHIR_JSON_NEW);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
}
private Bundle executeAndReturnLinkNext(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException {
CloseableHttpResponse status = ourClient.execute(httpGet);
Bundle bundle;
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertEquals(theExpectEncoding, ct);
bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size());
String linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertNotNull(linkNext);
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
return bundle;
}
@Test
public void testSearchWithPostAndInvalidParameters() {
IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
LoggingInterceptor interceptor = new LoggingInterceptor();
interceptor.setLogRequestSummary(true);
interceptor.setLogRequestBody(true);
interceptor.setLogRequestHeaders(false);
interceptor.setLogResponseBody(false);
interceptor.setLogResponseHeaders(false);
interceptor.setLogResponseSummary(false);
client.registerInterceptor(interceptor);
try {
client
.search()
.forResource(Patient.class)
.where(new StringClientParam("foo").matches().value("bar"))
.prettyPrint()
.usingStyle(SearchStyleEnum.POST)
.returnBundle(Bundle.class)
.encodedJson()
.execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Invalid request: The FHIR endpoint on this server does not know how to handle POST operation[Patient/_search] with parameters [[_pretty, foo]]"));
}
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@SuppressWarnings("rawtypes")
@Search()
public List search(
@RequiredParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) {
ourLastMethod = "search";
ourIdentifiers = theIdentifiers;
ArrayList<Patient> retVal = new ArrayList<Patient>();
for (int i = 0; i < 200; i++) {
Patient patient = new Patient();
patient.addName(new HumanName().setFamily("FAMILY"));
patient.getIdElement().setValue("Patient/" + i);
retVal.add(patient);
}
return retVal;
}
}
}

View File

@ -0,0 +1,131 @@
package ca.uhn.hapi.converters.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
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.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.model.HumanName;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
public class VersionedApiConverterInterceptorR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(VersionedApiConverterInterceptorR4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static int ourPort;
private static Server ourServer;
@Test
public void testSearchNormal() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertThat(responseContent, containsString("\"family\": \"FAMILY\""));
}
}
@Test
public void testSearchConvertToR2() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
httpGet.addHeader("Accept", "application/fhir+json; fhirVersion=1.0");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertThat(responseContent, containsString("\"family\": ["));
}
}
@Test
public void testSearchConvertToR2ByFormatParam() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=" + UrlUtil.escapeUrlParam("application/fhir+json; fhirVersion=1.0"));
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertThat(responseContent, containsString("\"family\": ["));
}
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setDefaultPrettyPrint(true);
servlet.registerInterceptor(new VersionedApiConverterInterceptor());
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@SuppressWarnings("rawtypes")
@Search()
public List search() {
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.getIdElement().setValue("Patient/A");
patient.addName(new HumanName().setFamily("FAMILY"));
retVal.add(patient);
return retVal;
}
}
}

View File

@ -0,0 +1,48 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<logger name="ca.uhn.fhir.jpa.dao.FhirResourceDaoSubscriptionDstu2" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.eclipse.jetty.websocket" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.eclipse" additivity="false" level="error">
</logger>
<logger name="ca.uhn.fhir.rest.client" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<logger name="ca.uhn.fhir.jpa.dao" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<!-- Set to 'trace' to enable SQL logging -->
<logger name="org.hibernate.SQL" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<!-- Set to 'trace' to enable SQL Value logging -->
<logger name="org.hibernate.type" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -91,7 +91,7 @@ public class JaxRsResponse extends RestfulResponse<JaxRsRequest> {
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
if (outcome != null) { if (outcome != null) {
FhirContext fhirContext = getRequestDetails().getServer().getFhirContext(); FhirContext fhirContext = getRequestDetails().getServer().getFhirContext();
IParser parser = RestfulServerUtils.getNewParser(fhirContext, getRequestDetails()); IParser parser = RestfulServerUtils.getNewParser(fhirContext, fhirContext.getVersion().getVersion(), getRequestDetails());
outcome.execute(parser, writer); outcome.execute(parser, writer);
} }
return sendWriterResponse(operationStatus, getParserType(), null, writer); return sendWriterResponse(operationStatus, getParserType(), null, writer);

View File

@ -155,6 +155,11 @@
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId> <artifactId>commons-dbcp2</artifactId>
</dependency> </dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-converter</artifactId>
<version>3.3.0-SNAPSHOT</version>
</dependency>
</dependencies> </dependencies>

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhirtest.config.*; import ca.uhn.fhirtest.config.*;
import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
@ -176,6 +177,11 @@ public class TestRestfulServer extends RestfulServer {
CorsInterceptor corsInterceptor = new CorsInterceptor(); CorsInterceptor corsInterceptor = new CorsInterceptor();
registerInterceptor(corsInterceptor); registerInterceptor(corsInterceptor);
/*
* Enable version conversion
*/
registerInterceptor(new VersionedApiConverterInterceptor());
/* /*
* We want to format the response using nice HTML if it's a browser, since this * We want to format the response using nice HTML if it's a browser, since this
* makes things a little easier for testers. * makes things a little easier for testers.

View File

@ -58,6 +58,7 @@ public class RestfulServerUtils {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class);
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<String>(Arrays.asList("Bundle", "*.text", "*.(mandatory)")); private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<String>(Arrays.asList("Bundle", "*.text", "*.(mandatory)"));
private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<FhirVersionEnum, FhirContext>());
public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) { public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
// Pretty print // Pretty print
@ -433,7 +434,9 @@ public class RestfulServerUtils {
if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) { if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) {
String resName = theResourceId.getResourceType(); String resName = theResourceId.getResourceType();
if (theResource != null && isBlank(resName)) { if (theResource != null && isBlank(resName)) {
resName = theServer.getFhirContext().getResourceDefinition(theResource).getName(); FhirContext context = theServer.getFhirContext();
context = getContextForVersion(context, theResource.getStructureFhirVersionEnum());
resName = context.getResourceDefinition(theResource).getName();
} }
if (isNotBlank(resName)) { if (isNotBlank(resName)) {
retVal = theResourceId.withServerBase(theServerBase, resName); retVal = theResourceId.withServerBase(theServerBase, resName);
@ -455,18 +458,19 @@ public class RestfulServerUtils {
return new ResponseEncoding(theFhirContext, encoding, theContentType); return new ResponseEncoding(theFhirContext, encoding, theContentType);
} }
public static IParser getNewParser(FhirContext theContext, RequestDetails theRequestDetails) { public static IParser getNewParser(FhirContext theContext, FhirVersionEnum theForVersion, RequestDetails theRequestDetails) {
FhirContext context = getContextForVersion(theContext, theForVersion);
// Determine response encoding // Determine response encoding
EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails).getEncoding(); EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails).getEncoding();
IParser parser; IParser parser;
switch (responseEncoding) { switch (responseEncoding) {
case JSON: case JSON:
parser = theContext.newJsonParser(); parser = context.newJsonParser();
break; break;
case XML: case XML:
default: default:
parser = theContext.newXmlParser(); parser = context.newXmlParser();
break; break;
} }
@ -475,6 +479,18 @@ public class RestfulServerUtils {
return parser; return parser;
} }
private static FhirContext getContextForVersion(FhirContext theContext, FhirVersionEnum theForVersion) {
FhirContext context = theContext;
if (context.getVersion().getVersion() != theForVersion) {
context = myFhirContextMap.get(theForVersion);
if (context == null) {
context = theForVersion.newContext();
myFhirContextMap.put(theForVersion, context);
}
}
return context;
}
public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) { public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) {
Set<String> retVal = new HashSet<String>(); Set<String> retVal = new HashSet<String>();
@ -689,7 +705,8 @@ public class RestfulServerUtils {
} else if (encodingDomainResourceAsText && theResource instanceof IResource) { } else if (encodingDomainResourceAsText && theResource instanceof IResource) {
writer.append(((IResource) theResource).getText().getDiv().getValueAsString()); writer.append(((IResource) theResource).getText().getDiv().getValueAsString());
} else { } else {
IParser parser = getNewParser(theServer.getFhirContext(), theRequestDetails); FhirVersionEnum forVersion = theResource.getStructureFhirVersionEnum();
IParser parser = getNewParser(theServer.getFhirContext(), forVersion, theRequestDetails);
parser.encodeResourceToWriter(theResource, writer); parser.encodeResourceToWriter(theResource, writer);
} }
//FIXME resource leak //FIXME resource leak

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.server.interceptor; package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
@ -392,7 +393,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
} }
} }
private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource resource, ServletRequest theServletRequest, int theStatusCode) { private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource theResource, ServletRequest theServletRequest, int theStatusCode) {
if (theRequestDetails.getServer() instanceof RestfulServer) { if (theRequestDetails.getServer() instanceof RestfulServer) {
RestfulServer rs = (RestfulServer) theRequestDetails.getServer(); RestfulServer rs = (RestfulServer) theRequestDetails.getServer();
@ -402,7 +403,8 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
IParser p; IParser p;
Map<String, String[]> parameters = theRequestDetails.getParameters(); Map<String, String[]> parameters = theRequestDetails.getParameters();
if (parameters.containsKey(Constants.PARAM_FORMAT)) { if (parameters.containsKey(Constants.PARAM_FORMAT)) {
p = RestfulServerUtils.getNewParser(theRequestDetails.getServer().getFhirContext(), theRequestDetails); FhirVersionEnum forVersion = theResource.getStructureFhirVersionEnum();
p = RestfulServerUtils.getNewParser(theRequestDetails.getServer().getFhirContext(), forVersion, theRequestDetails);
} else { } else {
EncodingEnum defaultResponseEncoding = theRequestDetails.getServer().getDefaultResponseEncoding(); EncodingEnum defaultResponseEncoding = theRequestDetails.getServer().getDefaultResponseEncoding();
p = defaultResponseEncoding.newParser(theRequestDetails.getServer().getFhirContext()); p = defaultResponseEncoding.newParser(theRequestDetails.getServer().getFhirContext());
@ -423,7 +425,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
} }
EncodingEnum encoding = p.getEncoding(); EncodingEnum encoding = p.getEncoding();
String encoded = p.encodeResourceToString(resource); String encoded = p.encodeResourceToString(theResource);
try { try {
@ -615,7 +617,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
b.append("\n"); b.append("\n");
InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js"); InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript resource not found')"; String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest()); jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
b.append("<script type=\"text/javascript\">"); b.append("<script type=\"text/javascript\">");
b.append(jsStr); b.append(jsStr);

View File

@ -74,6 +74,11 @@
it is possible to perform Schematron validation on Java 9. Thanks to it is possible to perform Schematron validation on Java 9. Thanks to
Mark Grimes for reporting and suggesting a fix! Mark Grimes for reporting and suggesting a fix!
</action> </action>
<action type="add">
An experimental interceptor called VersionedApiConverterInterceptor has been added,
which automaticaly converts response payloads to a client-specified version
according to transforms built into FHIR.
</action>
</release> </release>
<release version="3.2.0" date="2018-01-13"> <release version="3.2.0" date="2018-01-13">
<action type="add"> <action type="add">