From b442982310ef5402393d8a06fa151a54f03d4a7a Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sat, 8 Dec 2018 18:49:58 -0500 Subject: [PATCH] Add media interceptor --- .../ca/uhn/fhir/fluentpath/IFluentPath.java | 12 +- ...irResourceDaoCreatePlaceholdersR4Test.java | 62 +++++-- .../fhir/jpa/model/entity/ResourceTable.java | 7 +- .../uhn/fhir/rest/server/RestfulServer.java | 8 - .../fhir/rest/server/RestfulServerUtils.java | 168 ++++++++++-------- .../ServeMediaResourceRawInterceptor.java | 101 +++++++++++ .../rest/server/method/BaseMethodBinding.java | 2 + .../BaseResourceReturningMethodBinding.java | 33 +--- .../rest/server/method/ReadMethodBinding.java | 2 +- .../server/method/SearchMethodBinding.java | 23 +-- .../hapi/fluentpath/FluentPathDstu3.java | 6 + .../fhir/r4/hapi/fluentpath/FluentPathR4.java | 68 +++---- .../ServeMediaResourceRawInterceptorTest.java | 160 +++++++++++++++++ src/changes/changes.xml | 10 ++ 14 files changed, 485 insertions(+), 177 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java index b370ea014e1..8f2b8158781 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.fluentpath; */ import java.util.List; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBase; @@ -36,6 +37,15 @@ public interface IFluentPath { */ List evaluate(IBase theInput, String thePath, Class theReturnType); - + /** + * Apply the given FluentPath expression against the given input and return + * the first match (if any) + * + * @param theInput The input object (generally a resource or datatype) + * @param thePath The fluent path expression + * @param theReturnType The type to return (in order to avoid casting) + */ + Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType); + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java index 230c01712fc..57553c26da6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java @@ -1,23 +1,26 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; -import org.junit.*; +import org.hl7.fhir.r4.model.Task; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; -import java.util.*; +import java.util.List; -import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.contains; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; -@SuppressWarnings({ "unchecked", "deprecation" }) +@SuppressWarnings({"unchecked", "deprecation"}) public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCreatePlaceholdersR4Test.class); @@ -25,6 +28,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { @After public final void afterResetDao() { myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); } @Test @@ -97,7 +101,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { } @Test - public void testUpdateWithBadReferenceIsPermitted() { + public void testUpdateWithBadReferenceIsPermittedAlphanumeric() { assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); @@ -105,11 +109,49 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { o.setStatus(ObservationStatus.FINAL); IIdType id = myObservationDao.create(o, mySrd).getId(); + try { + myPatientDao.read(new IdType("Patient/FOO")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + o = new Observation(); o.setId(id); o.setStatus(ObservationStatus.FINAL); o.getSubject().setReference("Patient/FOO"); myObservationDao.update(o, mySrd); + + myPatientDao.read(new IdType("Patient/FOO")); + + } + + @Test + public void testUpdateWithBadReferenceIsPermittedNumeric() { + assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ANY); + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + IIdType id = myObservationDao.create(o, mySrd).getId(); + + try { + myPatientDao.read(new IdType("Patient/999999999999999")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + + o = new Observation(); + o.setId(id); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/999999999999999"); + myObservationDao.update(o, mySrd); + + + myPatientDao.read(new IdType("Patient/999999999999999")); + } @AfterClass diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index 2093f66986c..e59ea7add71 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -610,12 +610,9 @@ public class ResourceTable extends BaseHasResource implements Serializable { if (myHasLinks && myResourceLinks != null) { myResourceLinksField = getResourceLinks() .stream() - .map(t->{ - Long retVal = t.getTargetResourcePid(); - return retVal; - }) + .map(ResourceLink::getTargetResourcePid) .filter(Objects::nonNull) - .map(t->t.toString()) + .map(Object::toString) .collect(Collectors.joining(" ")); } else { myResourceLinksField = null; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index fd191539ebe..baf83dca891 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -180,12 +180,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer TEXT_ENCODE_ELEMENTS = new HashSet(Arrays.asList("Bundle", "*.text", "*.(mandatory)")); private static Map myFhirContextMap = Collections.synchronizedMap(new HashMap()); + private enum NarrativeModeEnum { + NORMAL, ONLY, SUPPRESS; + + public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { + return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); + } + } + + /** + * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} + */ + public static class ResponseEncoding { + private final String myContentType; + private final EncodingEnum myEncoding; + private final Boolean myNonLegacy; + + public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { + super(); + myEncoding = theEncoding; + myContentType = theContentType; + if (theContentType != null) { + FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); + if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { + myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); + } else { + myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); + } + } else { + FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); + if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { + myNonLegacy = null; + } else { + myNonLegacy = Boolean.TRUE; + } + } + } + + public String getContentType() { + return myContentType; + } + + public EncodingEnum getEncoding() { + return myEncoding; + } + + public String getResourceContentType() { + if (Boolean.TRUE.equals(isNonLegacy())) { + return getEncoding().getResourceContentTypeNonLegacy(); + } + return getEncoding().getResourceContentType(); + } + + Boolean isNonLegacy() { + return myNonLegacy; + } + } + public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) { // Pretty print boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails); @@ -272,6 +329,15 @@ public class RestfulServerUtils { * equally, returns thePrefer. */ public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) { + return determineResponseEncodingNoDefault(theReq, thePrefer, null); + } + + /** + * Try to determing the response content type, given the request Accept header and + * _format parameter. If a value is provided to thePreferContents, we'll + * prefer to return that value over the native FHIR value. + */ + public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer, String thePreferContentType) { String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT); if (format != null) { for (String nextFormat : format) { @@ -333,12 +399,12 @@ public class RestfulServerUtils { ResponseEncoding encoding; if (endSpaceIndex == -1) { if (startSpaceIndex == 0) { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken, thePreferContentType); } else { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex)); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex), thePreferContentType); } } else { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex)); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex), thePreferContentType); String remaining = nextToken.substring(endSpaceIndex + 1); StringTokenizer qualifierTok = new StringTokenizer(remaining, ";"); while (qualifierTok.hasMoreTokens()) { @@ -476,13 +542,18 @@ public class RestfulServerUtils { return context; } - private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) { + private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType, String thePreferContentType) { EncodingEnum encoding; if (theStrict) { encoding = EncodingEnum.forContentTypeStrict(theContentType); } else { encoding = EncodingEnum.forContentType(theContentType); } + if (isNotBlank(thePreferContentType)) { + if (thePreferContentType.equals(theContentType)) { + return new ResponseEncoding(theFhirContext, encoding, theContentType); + } + } if (encoding == null) { return null; } @@ -749,23 +820,6 @@ public class RestfulServerUtils { return response.sendWriterResponse(theStatusCode, contentType, charset, writer); } - public static String createEtag(String theVersionId) { - return "W/\"" + theVersionId + '"'; - } - - public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { - String[] retVal = theRequest.getParameters().get(theParamName); - if (retVal == null) { - return null; - } - try { - return Integer.parseInt(retVal[0]); - } catch (NumberFormatException e) { - ourLog.debug("Failed to parse {} value '{}': {}", new Object[] {theParamName, retVal[0], e}); - return null; - } - } - // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) { // String countString = theRequest.getParameter(name); // Integer count = null; @@ -779,61 +833,27 @@ public class RestfulServerUtils { // return count; // } + public static String createEtag(String theVersionId) { + return "W/\"" + theVersionId + '"'; + } + + public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { + String[] retVal = theRequest.getParameters().get(theParamName); + if (retVal == null) { + return null; + } + try { + return Integer.parseInt(retVal[0]); + } catch (NumberFormatException e) { + ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e}); + return null; + } + } + public static void validateResourceListNotNull(List theResourceList) { if (theResourceList == null) { throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); } } - private enum NarrativeModeEnum { - NORMAL, ONLY, SUPPRESS; - - public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { - return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); - } - } - - /** - * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} - */ - public static class ResponseEncoding { - private final EncodingEnum myEncoding; - private final Boolean myNonLegacy; - - public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { - super(); - myEncoding = theEncoding; - if (theContentType != null) { - FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); - if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { - myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); - } else { - myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); - } - } else { - FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); - if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { - myNonLegacy = null; - } else { - myNonLegacy = Boolean.TRUE; - } - } - } - - public EncodingEnum getEncoding() { - return myEncoding; - } - - public String getResourceContentType() { - if (Boolean.TRUE.equals(isNonLegacy())) { - return getEncoding().getResourceContentTypeNonLegacy(); - } - return getEncoding().getResourceContentType(); - } - - public Boolean isNonLegacy() { - return myNonLegacy; - } - } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java new file mode 100644 index 00000000000..5e5784cabf6 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.rest.server.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +/** + * This interceptor allows a client to request that a Media resource be + * served as the raw contents of the resource, assuming either: + *
    + *
  • The client explicitly requests the correct content type using the Accept header
  • + *
  • The client explicitly requests raw output by adding the parameter _output=data
  • + *
+ */ +public class ServeMediaResourceRawInterceptor extends InterceptorAdapter { + + public static final String MEDIA_CONTENT_CONTENT_TYPE_OPT = "Media.content.contentType"; + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + if (theResponseObject == null) { + return true; + } + + + FhirContext context = theRequestDetails.getFhirContext(); + String resourceName = context.getResourceDefinition(theResponseObject).getName(); + + // Are we serving a FHIR read request on the Media resource type + if (!"Media".equals(resourceName) || theRequestDetails.getRestOperationType() != RestOperationTypeEnum.READ) { + return true; + } + + // What is the content type of the Media resource we're returning? + String contentType = null; + Optional contentTypeOpt = context.newFluentPath().evaluateFirst(theResponseObject, MEDIA_CONTENT_CONTENT_TYPE_OPT, IPrimitiveType.class); + if (contentTypeOpt.isPresent()) { + contentType = contentTypeOpt.get().getValueAsString(); + } + + // What is the data of the Media resource we're returning? + IPrimitiveType data = null; + Optional dataOpt = context.newFluentPath().evaluateFirst(theResponseObject, "Media.content.data", IPrimitiveType.class); + if (dataOpt.isPresent()) { + data = dataOpt.get(); + } + + if (isBlank(contentType) || data == null) { + return true; + } + + RestfulServerUtils.ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, null, contentType); + if (responseEncoding != null) { + if (contentType.equals(responseEncoding.getContentType())) { + returnRawResponse(theRequestDetails, theServletResponse, contentType, data); + return false; + + } + } + + String[] outputParam = theRequestDetails.getParameters().get("_output"); + if (outputParam != null && "data".equals(outputParam[0])) { + returnRawResponse(theRequestDetails, theServletResponse, contentType, data); + return false; + } + + return true; + } + + private void returnRawResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, String theContentType, IPrimitiveType theData) { + theServletResponse.setStatus(200); + if (theRequestDetails.getServer() instanceof RestfulServer) { + RestfulServer rs = (RestfulServer) theRequestDetails.getServer(); + rs.addHeadersToResponse(theServletResponse); + } + + theServletResponse.addHeader(Constants.HEADER_CONTENT_TYPE, theContentType); + + // Write the response + try { + theServletResponse.getOutputStream().write(theData.getValue()); + theServletResponse.getOutputStream().close(); + } catch (IOException e) { + throw new InternalErrorException(e); + } + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 66044200232..d5a81fb6e91 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -84,6 +84,8 @@ public abstract class BaseMethodBinding { } } + // This allows us to invoke methods on private classes + myMethod.setAccessible(true); } protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List> thePreferTypes) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 19b8a282812..dbe95b2e4ac 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -19,7 +19,6 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.UrlUtil; @@ -45,9 +44,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -57,27 +56,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { - protected static final Set ALLOWED_PARAMS; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); - static { - HashSet set = new HashSet(); - set.add(Constants.PARAM_FORMAT); - set.add(Constants.PARAM_NARRATIVE); - set.add(Constants.PARAM_PRETTY); - set.add(Constants.PARAM_SORT); - set.add(Constants.PARAM_SORT_ASC); - set.add(Constants.PARAM_SORT_DESC); - set.add(Constants.PARAM_COUNT); - set.add(Constants.PARAM_SUMMARY); - set.add(Constants.PARAM_ELEMENTS); - set.add(ResponseHighlighterInterceptor.PARAM_RAW); - ALLOWED_PARAMS = Collections.unmodifiableSet(set); - } - private MethodReturnTypeEnum myMethodReturnType; private String myResourceName; - private Class myResourceType; @SuppressWarnings("unchecked") public BaseResourceReturningMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { @@ -112,11 +94,12 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi if (theReturnResourceType != null) { if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { - if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) { - // If we're returning an abstract type, that's ok - } else { - myResourceType = (Class) theReturnResourceType; - myResourceName = theContext.getResourceDefinition(myResourceType).getName(); + + // If we're returning an abstract type, that's ok, but if we know the resource + // type let's grab it + if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) && !Modifier.isInterface(theReturnResourceType.getModifiers())) { + Class resourceType = (Class) theReturnResourceType; + myResourceName = theContext.getResourceDefinition(resourceType).getName(); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java index 9d3eb9229b8..dacce858677 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java @@ -110,7 +110,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding { return false; } for (String next : theRequest.getParameters().keySet()) { - if (!ALLOWED_PARAMS.contains(next)) { + if (!next.startsWith("_")) { return false; } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index 3b06a9b2f19..d42cc294b91 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -75,27 +75,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } } - /* - * Check for parameter combinations and names that are invalid - */ - List parameters = getParameters(); - for (int i = 0; i < parameters.size(); i++) { - IParameter next = parameters.get(i); - if (!(next instanceof SearchParameter)) { - continue; - } - - SearchParameter sp = (SearchParameter) next; - if (sp.getName().startsWith("_")) { - if (ALLOWED_PARAMS.contains(sp.getName())) { - String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(), - sp.getName()); - throw new ConfigurationException(msg); - } - } - - } - /* * Only compartment searching methods may have an ID parameter */ @@ -232,7 +211,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } } for (String next : theRequest.getParameters().keySet()) { - if (ALLOWED_PARAMS.contains(next)) { + if (next.startsWith("_")) { methodParamsTemp.add(next); } } diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java index ee8baaccde0..11da788d945 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java @@ -11,6 +11,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import java.util.List; +import java.util.Optional; public class FluentPathDstu3 implements IFluentPath { @@ -43,4 +44,9 @@ public class FluentPathDstu3 implements IFluentPath { return (List) result; } + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } + } diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java index ae5dcbba6a7..45e9aa7e27f 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java @@ -1,7 +1,8 @@ package org.hl7.fhir.r4.hapi.fluentpath; -import java.util.List; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fluentpath.FluentPathExecutionException; +import ca.uhn.fhir.fluentpath.IFluentPath; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; @@ -9,39 +10,44 @@ import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.utils.FHIRPathEngine; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.fluentpath.FluentPathExecutionException; -import ca.uhn.fhir.fluentpath.IFluentPath; +import java.util.List; +import java.util.Optional; -public class FluentPathR4 implements IFluentPath { +public class FluentPathR4 implements IFluentPath { - private FHIRPathEngine myEngine; + private FHIRPathEngine myEngine; - public FluentPathR4(FhirContext theCtx) { - if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { - throw new IllegalStateException("Validation support module configured on context appears to be for the wrong FHIR version- Does not extend " + IValidationSupport.class.getName()); - } - IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport(); - myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); - } + public FluentPathR4(FhirContext theCtx) { + if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { + throw new IllegalStateException("Validation support module configured on context appears to be for the wrong FHIR version- Does not extend " + IValidationSupport.class.getName()); + } + IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport(); + myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); + } - @SuppressWarnings("unchecked") - @Override - public List evaluate(IBase theInput, String thePath, Class theReturnType) { - List result; - try { - result = myEngine.evaluate((Base)theInput, thePath); - } catch (FHIRException e) { - throw new FluentPathExecutionException(e); - } + @SuppressWarnings("unchecked") + @Override + public List evaluate(IBase theInput, String thePath, Class theReturnType) { + List result; + try { + result = myEngine.evaluate((Base) theInput, thePath); + } catch (FHIRException e) { + throw new FluentPathExecutionException(e); + } + + for (Base next : result) { + if (!theReturnType.isAssignableFrom(next.getClass())) { + throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); + } + } + + return (List) result; + } + + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } - for (Base next : result) { - if (!theReturnType.isAssignableFrom(next.getClass())) { - throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); - } - } - - return (List) result; - } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java new file mode 100644 index 00000000000..469ead376ed --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java @@ -0,0 +1,160 @@ +package ca.uhn.fhir.rest.server.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +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 com.google.common.base.Charsets; +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.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Media; +import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.*; + +public class ServeMediaResourceRawInterceptorTest { + + + private static final Logger ourLog = LoggerFactory.getLogger(ServeMediaResourceRawInterceptorTest.class); + private static int ourPort; + private static RestfulServer ourServlet; + private static FhirContext ourCtx = FhirContext.forR4(); + private static CloseableHttpClient ourClient; + private static Media ourNextResponse; + private static String ourReadUrl; + private ServeMediaResourceRawInterceptor myInterceptor; + + @Before + public void before() { + myInterceptor = new ServeMediaResourceRawInterceptor(); + ourServlet.registerInterceptor(myInterceptor); + } + + @After + public void after() { + ourNextResponse = null; + ourServlet.unregisterInterceptor(myInterceptor); + } + + @Test + public void testMediaHasImageRequestHasNoAcceptHeader() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setContentType("image/png"); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("application/fhir+json;charset=utf-8", response.getEntity().getContentType().getValue()); + String contents = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + assertThat(contents, containsString("\"resourceType\"")); + } + } + + @Test + public void testMediaHasImageRequestHasMatchingAcceptHeader() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setContentType("image/png"); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl); + get.addHeader(Constants.HEADER_ACCEPT, "image/png"); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("image/png", response.getEntity().getContentType().getValue()); + byte[] contents = IOUtils.toByteArray(response.getEntity().getContent()); + assertArrayEquals(new byte[]{2, 3, 4, 5, 6, 7, 8}, contents); + } + } + + @Test + public void testMediaHasNoContentType() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl); + get.addHeader(Constants.HEADER_ACCEPT, "image/png"); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("application/fhir+json;charset=utf-8", response.getEntity().getContentType().getValue()); + } + } + + @Test + public void testMediaHasImageRequestHasNonMatchingAcceptHeaderOutputRaw() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setContentType("image/png"); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl + "?_output=data"); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("image/png", response.getEntity().getContentType().getValue()); + byte[] contents = IOUtils.toByteArray(response.getEntity().getContent()); + assertArrayEquals(new byte[]{2, 3, 4, 5, 6, 7, 8}, contents); + } + } + + private static class MyMediaResourceProvider implements IResourceProvider { + + + @Override + public Class getResourceType() { + return Media.class; + } + + @Read + public Media read(@IdParam IIdType theId) { + return ourNextResponse; + } + + } + + @AfterClass + public static void afterClassClearContext() throws IOException { + ourClient.close(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + + // Create server + ourLog.info("Using port: {}", ourPort); + Server ourServer = new Server(ourPort); + ServletHandler proxyHandler = new ServletHandler(); + ourServlet = new RestfulServer(ourCtx); + ourServlet.setDefaultResponseEncoding(EncodingEnum.JSON); + ourServlet.setResourceProviders(new MyMediaResourceProvider()); + ServletHolder servletHolder = new ServletHolder(ourServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + // Create client + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + ourReadUrl = "http://localhost:" + ourPort + "/Media/123"; + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index fcb7c7624be..db2d2cf4157 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -132,6 +132,16 @@ An issue was corrected with the JPA reindexer, where String index columns do not always get reindexed if they did not have an identity hash value in the HASH_IDENTITY column. + + Plain Server ResourceProvider classes are no longer required to be public classes. This + limitation has always been enforced, but did not actually serve any real purpose so it + has been removed. + + + A new interceptor called ServeMediaResourceRawInterceptor has been added. This interceptor + causes Media resources to be served as raw content if the client explicitly requests + the correct content type cia the Accept header. +