From e52f5827693f8b18788da449995cc7eaa19b3bb0 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 29 Jan 2018 12:10:05 -0600 Subject: [PATCH] An experimental interceptor called VersionedApiConverterInterceptor has been added, which automaticaly converts response payloads to a client-specified version according to transforms built into FHIR. --- .../ca/uhn/fhir/context/FhirVersionEnum.java | 81 ++--- hapi-fhir-converter/pom.xml | 11 + .../VersionedApiConverterInterceptor.java | 96 ++++-- .../NullVersionConverterAdvisor40.java | 56 ++++ .../converters/server/SearchDstu3Test.java | 276 ------------------ ...ersionedApiConverterInterceptorR4Test.java | 131 +++++++++ .../src/test/resources/logback-test.xml | 48 +++ .../fhir/jaxrs/server/util/JaxRsResponse.java | 2 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 5 + .../ca/uhn/fhirtest/TestRestfulServer.java | 6 + .../fhir/rest/server/RestfulServerUtils.java | 33 ++- .../ResponseHighlighterInterceptor.java | 10 +- src/changes/changes.xml | 5 + 13 files changed, 412 insertions(+), 348 deletions(-) create mode 100644 hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/NullVersionConverterAdvisor40.java delete mode 100644 hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/SearchDstu3Test.java create mode 100644 hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptorR4Test.java create mode 100644 hapi-fhir-converter/src/test/resources/logback-test.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java index fafd274cdcb..6b9c2a09fa4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.context; * 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. @@ -40,16 +40,14 @@ public enum FhirVersionEnum { DSTU2_1("org.hl7.fhir.dstu2016may.hapi.ctx.FhirDstu2_1", null, true, new Version("1.4.0")), - DSTU3("org.hl7.fhir.dstu3.hapi.ctx.FhirDstu3", null, true, new Dstu3Version()), - - R4("org.hl7.fhir.r4.hapi.ctx.FhirR4", null, true, new R4Version()), - - ; + DSTU3("org.hl7.fhir.dstu3.hapi.ctx.FhirDstu3", null, true, new Dstu3Version()), + + R4("org.hl7.fhir.r4.hapi.ctx.FhirR4", null, true, new R4Version()),; private final FhirVersionEnum myEquivalent; private final boolean myIsRi; - private volatile Boolean myPresentOnClasspath; private final String myVersionClass; + private volatile Boolean myPresentOnClasspath; private volatile IFhirVersion myVersionImplementation; private String myFhirVersionString; @@ -82,21 +80,6 @@ public enum FhirVersionEnum { 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) { if (this.equals(theVersion)) { return true; @@ -139,15 +122,50 @@ public enum FhirVersionEnum { 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 String myVersion; + public Version(String theVersion) { super(); myVersion = theVersion; } - private String myVersion; - @Override public String provideVersion() { 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 * classes in order to supply an accurate version string even over time - * */ private static class Dstu3Version implements IVersionProvider { + private String myVersion; + public Dstu3Version() { try { Class c = Class.forName("org.hl7.fhir.dstu3.model.Constants"); @@ -175,8 +190,6 @@ public enum FhirVersionEnum { } } - private String myVersion; - @Override public String provideVersion() { return myVersion; @@ -186,6 +199,8 @@ public enum FhirVersionEnum { private static class R4Version implements IVersionProvider { + private String myVersion; + public R4Version() { try { Class c = Class.forName("org.hl7.fhir.r4.model.Constants"); @@ -195,8 +210,6 @@ public enum FhirVersionEnum { } } - private String myVersion; - @Override public String provideVersion() { return myVersion; diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 3be9a274a5d..d57d06bca18 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -32,6 +32,12 @@ provided + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu2 + 3.3.0-SNAPSHOT + true + ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 @@ -70,6 +76,11 @@ + + ch.qos.logback + logback-classic + test + ca.uhn.hapi.fhir hapi-fhir-client diff --git a/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptor.java b/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptor.java index 04a4cd432e5..bdebfb7db7e 100644 --- a/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptor.java +++ b/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptor.java @@ -1,39 +1,68 @@ package ca.uhn.hapi.converters.server; +import ca.uhn.fhir.context.FhirContext; 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.SummaryEnum; 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.InternalErrorException; 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.instance.model.api.IBaseResource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Collections; -import java.util.Set; import java.util.StringTokenizer; -import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.*; +/** + * This is an experimental interceptor! Use with caution as + * behaviour may change or be removed in a future version of + * FHIR. + *

+ * This interceptor partially implements the proposed + * Versioned API features. + *

+ */ 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 - public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { - String accept = defaultString(theServletRequest.getHeader(Constants.HEADER_ACCEPT)); + public boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + 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, ";"); String wantVersionString = null; while (tok.hasMoreTokens()) { String next = tok.nextToken().trim(); - if (next.startsWith("fhir-version=")) { - wantVersionString = next.substring("fhir-version=".length()).trim(); + if (next.startsWith("fhirVersion=")) { + wantVersionString = next.substring("fhirVersion=".length()).trim(); break; } } @@ -42,31 +71,48 @@ public class VersionedApiConverterInterceptor extends InterceptorAdapter { if (isNotBlank(wantVersionString)) { wantVersion = FhirVersionEnum.forVersionString(wantVersionString); } - FhirVersionEnum haveVersion = theResponseObject.getStructureFhirVersionEnum(); + + IBaseResource responseResource = theResponseDetails.getResponseResource(); + FhirVersionEnum haveVersion = responseResource.getStructureFhirVersionEnum(); IBaseResource converted = null; try { 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) { - converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.r4.model.Resource) theResponseObject); - } 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.DSTU2 && haveVersion == FhirVersionEnum.R4) { + 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) { throw new InternalErrorException(e); } if (converted != null) { - Set objects = Collections.emptySet(); - try { - RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), converted, objects, 200, "OK", false, false, theRequestDetails, null, null); - return false; - } catch (IOException e) { - throw new InternalErrorException(e); - } + theResponseDetails.setResponseResource(converted); } 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; + } } diff --git a/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/NullVersionConverterAdvisor40.java b/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/NullVersionConverterAdvisor40.java new file mode 100644 index 00000000000..22219b57aae --- /dev/null +++ b/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/NullVersionConverterAdvisor40.java @@ -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; + } + +} diff --git a/hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/SearchDstu3Test.java b/hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/SearchDstu3Test.java deleted file mode 100644 index ed659016d8a..00000000000 --- a/hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/SearchDstu3Test.java +++ /dev/null @@ -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 getResourceType() { - return Patient.class; - } - - @SuppressWarnings("rawtypes") - @Search() - public List search( - @RequiredParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) { - ourLastMethod = "search"; - ourIdentifiers = theIdentifiers; - ArrayList retVal = new ArrayList(); - - 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; - } - - } - -} diff --git a/hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptorR4Test.java b/hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptorR4Test.java new file mode 100644 index 00000000000..c6c357b2bad --- /dev/null +++ b/hapi-fhir-converter/src/test/java/ca/uhn/hapi/converters/server/VersionedApiConverterInterceptorR4Test.java @@ -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 getResourceType() { + return Patient.class; + } + + @SuppressWarnings("rawtypes") + @Search() + public List search() { + ArrayList retVal = new ArrayList<>(); + + Patient patient = new Patient(); + patient.getIdElement().setValue("Patient/A"); + patient.addName(new HumanName().setFamily("FAMILY")); + retVal.add(patient); + + return retVal; + } + + } + +} diff --git a/hapi-fhir-converter/src/test/resources/logback-test.xml b/hapi-fhir-converter/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..91f8a74d3e2 --- /dev/null +++ b/hapi-fhir-converter/src/test/resources/logback-test.xml @@ -0,0 +1,48 @@ + + + + + INFO + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java index 9071b801603..182421e83c0 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -91,7 +91,7 @@ public class JaxRsResponse extends RestfulResponse { StringWriter writer = new StringWriter(); if (outcome != null) { FhirContext fhirContext = getRequestDetails().getServer().getFhirContext(); - IParser parser = RestfulServerUtils.getNewParser(fhirContext, getRequestDetails()); + IParser parser = RestfulServerUtils.getNewParser(fhirContext, fhirContext.getVersion().getVersion(), getRequestDetails()); outcome.execute(parser, writer); } return sendWriterResponse(operationStatus, getParserType(), null, writer); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 025558219df..2dfc1ec5008 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -155,6 +155,11 @@ org.apache.commons commons-dbcp2
+ + ca.uhn.hapi.fhir + hapi-fhir-converter + 3.3.0-SNAPSHOT + diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index a77e7c7b5de..e564b337c23 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -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.ResponseHighlighterInterceptor; import ca.uhn.fhirtest.config.*; +import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor; import org.apache.commons.lang3.StringUtils; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; @@ -176,6 +177,11 @@ public class TestRestfulServer extends RestfulServer { CorsInterceptor corsInterceptor = new 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 * makes things a little easier for testers. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index 9f443e19a8b..c79de493441 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server; * 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. @@ -58,6 +58,7 @@ public class RestfulServerUtils { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class); private static final HashSet TEXT_ENCODE_ELEMENTS = new HashSet(Arrays.asList("Bundle", "*.text", "*.(mandatory)")); + private static Map myFhirContextMap = Collections.synchronizedMap(new HashMap()); public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) { // Pretty print @@ -266,7 +267,7 @@ public class RestfulServerUtils { * Some browsers (e.g. FF) request "application/xml" in their Accept header, * and we generally want to treat this as a preference for FHIR XML even if * it's not the FHIR version of the CT, which should be "application/xml+fhir". - * + * * When we're serving up Binary resources though, we are a bit more strict, * since Binary is supposed to use native content types unless the client has * explicitly requested FHIR. @@ -433,7 +434,9 @@ public class RestfulServerUtils { if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) { String resName = theResourceId.getResourceType(); 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)) { retVal = theResourceId.withServerBase(theServerBase, resName); @@ -455,18 +458,19 @@ public class RestfulServerUtils { 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 EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails).getEncoding(); IParser parser; switch (responseEncoding) { case JSON: - parser = theContext.newJsonParser(); + parser = context.newJsonParser(); break; case XML: default: - parser = theContext.newXmlParser(); + parser = context.newXmlParser(); break; } @@ -475,6 +479,18 @@ public class RestfulServerUtils { 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 parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) { Set retVal = new HashSet(); @@ -689,7 +705,8 @@ public class RestfulServerUtils { } else if (encodingDomainResourceAsText && theResource instanceof IResource) { writer.append(((IResource) theResource).getText().getDiv().getValueAsString()); } else { - IParser parser = getNewParser(theServer.getFhirContext(), theRequestDetails); + FhirVersionEnum forVersion = theResource.getStructureFhirVersionEnum(); + IParser parser = getNewParser(theServer.getFhirContext(), forVersion, theRequestDetails); parser.encodeResourceToWriter(theResource, writer); } //FIXME resource leak diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index 997dccf02d8..2624b5c223e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server.interceptor; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; 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) { RestfulServer rs = (RestfulServer) theRequestDetails.getServer(); @@ -402,7 +403,8 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { IParser p; Map parameters = theRequestDetails.getParameters(); 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 { EncodingEnum defaultResponseEncoding = theRequestDetails.getServer().getDefaultResponseEncoding(); p = defaultResponseEncoding.newParser(theRequestDetails.getServer().getFhirContext()); @@ -423,7 +425,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { } EncodingEnum encoding = p.getEncoding(); - String encoded = p.encodeResourceToString(resource); + String encoded = p.encodeResourceToString(theResource); try { @@ -615,7 +617,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { b.append("\n"); 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()); b.append("