Add media interceptor

This commit is contained in:
James Agnew 2018-12-08 18:49:58 -05:00
parent 5a80e70d93
commit b442982310
14 changed files with 485 additions and 177 deletions

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.fluentpath;
*/ */
import java.util.List; import java.util.List;
import java.util.Optional;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
@ -36,6 +37,15 @@ public interface IFluentPath {
*/ */
<T extends IBase> List<T> evaluate(IBase theInput, String thePath, Class<T> theReturnType); <T extends IBase> List<T> evaluate(IBase theInput, String thePath, Class<T> 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)
*/
<T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> theReturnType);
} }

View File

@ -1,23 +1,26 @@
package ca.uhn.fhir.jpa.dao.r4; 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.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType; 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.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.hamcrest.Matchers.contains;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
@SuppressWarnings({ "unchecked", "deprecation" }) @SuppressWarnings({"unchecked", "deprecation"})
public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCreatePlaceholdersR4Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCreatePlaceholdersR4Test.class);
@ -25,6 +28,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
@After @After
public final void afterResetDao() { public final void afterResetDao() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets());
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
} }
@Test @Test
@ -97,7 +101,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
} }
@Test @Test
public void testUpdateWithBadReferenceIsPermitted() { public void testUpdateWithBadReferenceIsPermittedAlphanumeric() {
assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets());
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
@ -105,11 +109,49 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
o.setStatus(ObservationStatus.FINAL); o.setStatus(ObservationStatus.FINAL);
IIdType id = myObservationDao.create(o, mySrd).getId(); IIdType id = myObservationDao.create(o, mySrd).getId();
try {
myPatientDao.read(new IdType("Patient/FOO"));
fail();
} catch (ResourceNotFoundException e) {
// good
}
o = new Observation(); o = new Observation();
o.setId(id); o.setId(id);
o.setStatus(ObservationStatus.FINAL); o.setStatus(ObservationStatus.FINAL);
o.getSubject().setReference("Patient/FOO"); o.getSubject().setReference("Patient/FOO");
myObservationDao.update(o, mySrd); 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 @AfterClass

View File

@ -610,12 +610,9 @@ public class ResourceTable extends BaseHasResource implements Serializable {
if (myHasLinks && myResourceLinks != null) { if (myHasLinks && myResourceLinks != null) {
myResourceLinksField = getResourceLinks() myResourceLinksField = getResourceLinks()
.stream() .stream()
.map(t->{ .map(ResourceLink::getTargetResourcePid)
Long retVal = t.getTargetResourcePid();
return retVal;
})
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(t->t.toString()) .map(Object::toString)
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
} else { } else {
myResourceLinksField = null; myResourceLinksField = null;

View File

@ -180,12 +180,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
} }
private void assertProviderIsValid(Object theNext) throws ConfigurationException {
if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) {
throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class must be public");
}
}
public RestulfulServerConfiguration createConfiguration() { public RestulfulServerConfiguration createConfiguration() {
RestulfulServerConfiguration result = new RestulfulServerConfiguration(); RestulfulServerConfiguration result = new RestulfulServerConfiguration();
result.setResourceBindings(getResourceBindings()); result.setResourceBindings(getResourceBindings());
@ -1421,14 +1415,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
if (!newResourceProviders.isEmpty()) { if (!newResourceProviders.isEmpty()) {
ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myResourceProviders.size()); ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myResourceProviders.size());
for (IResourceProvider provider : newResourceProviders) { for (IResourceProvider provider : newResourceProviders) {
assertProviderIsValid(provider);
findResourceMethods(provider); findResourceMethods(provider);
} }
} }
if (!newPlainProviders.isEmpty()) { if (!newPlainProviders.isEmpty()) {
ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size(), myPlainProviders.size()); ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size(), myPlainProviders.size());
for (Object provider : newPlainProviders) { for (Object provider : newPlainProviders) {
assertProviderIsValid(provider);
findResourceMethods(provider); findResourceMethods(provider);
} }
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -57,6 +57,63 @@ public class RestfulServerUtils {
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>()); private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<FhirVersionEnum, FhirContext>());
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) { public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) {
// Pretty print // Pretty print
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails); boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails);
@ -272,6 +329,15 @@ public class RestfulServerUtils {
* equally, returns thePrefer. * equally, returns thePrefer.
*/ */
public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum 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); String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT);
if (format != null) { if (format != null) {
for (String nextFormat : format) { for (String nextFormat : format) {
@ -333,12 +399,12 @@ public class RestfulServerUtils {
ResponseEncoding encoding; ResponseEncoding encoding;
if (endSpaceIndex == -1) { if (endSpaceIndex == -1) {
if (startSpaceIndex == 0) { if (startSpaceIndex == 0) {
encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken); encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken, thePreferContentType);
} else { } else {
encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex)); encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex), thePreferContentType);
} }
} else { } 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); String remaining = nextToken.substring(endSpaceIndex + 1);
StringTokenizer qualifierTok = new StringTokenizer(remaining, ";"); StringTokenizer qualifierTok = new StringTokenizer(remaining, ";");
while (qualifierTok.hasMoreTokens()) { while (qualifierTok.hasMoreTokens()) {
@ -476,13 +542,18 @@ public class RestfulServerUtils {
return context; 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; EncodingEnum encoding;
if (theStrict) { if (theStrict) {
encoding = EncodingEnum.forContentTypeStrict(theContentType); encoding = EncodingEnum.forContentTypeStrict(theContentType);
} else { } else {
encoding = EncodingEnum.forContentType(theContentType); encoding = EncodingEnum.forContentType(theContentType);
} }
if (isNotBlank(thePreferContentType)) {
if (thePreferContentType.equals(theContentType)) {
return new ResponseEncoding(theFhirContext, encoding, theContentType);
}
}
if (encoding == null) { if (encoding == null) {
return null; return null;
} }
@ -749,23 +820,6 @@ public class RestfulServerUtils {
return response.sendWriterResponse(theStatusCode, contentType, charset, writer); 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) { // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) {
// String countString = theRequest.getParameter(name); // String countString = theRequest.getParameter(name);
// Integer count = null; // Integer count = null;
@ -779,61 +833,27 @@ public class RestfulServerUtils {
// return count; // 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<? extends IBaseResource> theResourceList) { public static void validateResourceListNotNull(List<? extends IBaseResource> theResourceList) {
if (theResourceList == null) { if (theResourceList == null) {
throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); 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;
}
}
} }

View File

@ -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:
* <ul>
* <li>The client explicitly requests the correct content type using the Accept header</li>
* <li>The client explicitly requests raw output by adding the parameter <code>_output=data</code></li>
* </ul>
*/
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<IPrimitiveType> 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<byte[]> data = null;
Optional<IPrimitiveType> 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<byte[]> 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);
}
}
}

View File

@ -84,6 +84,8 @@ public abstract class BaseMethodBinding<T> {
} }
} }
// This allows us to invoke methods on private classes
myMethod.setAccessible(true);
} }
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) { protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) {

View File

@ -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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
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.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.UrlUtil; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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<Object> { public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> {
protected static final Set<String> ALLOWED_PARAMS;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class);
static {
HashSet<String> set = new HashSet<String>();
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 MethodReturnTypeEnum myMethodReturnType;
private String myResourceName; private String myResourceName;
private Class<? extends IBaseResource> myResourceType;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
@ -112,11 +94,12 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
if (theReturnResourceType != null) { if (theReturnResourceType != null) {
if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) {
if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) {
// If we're returning an abstract type, that's ok // If we're returning an abstract type, that's ok, but if we know the resource
} else { // type let's grab it
myResourceType = (Class<? extends IResource>) theReturnResourceType; if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) && !Modifier.isInterface(theReturnResourceType.getModifiers())) {
myResourceName = theContext.getResourceDefinition(myResourceType).getName(); Class<? extends IBaseResource> resourceType = (Class<? extends IResource>) theReturnResourceType;
myResourceName = theContext.getResourceDefinition(resourceType).getName();
} }
} }
} }

View File

@ -110,7 +110,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
return false; return false;
} }
for (String next : theRequest.getParameters().keySet()) { for (String next : theRequest.getParameters().keySet()) {
if (!ALLOWED_PARAMS.contains(next)) { if (!next.startsWith("_")) {
return false; return false;
} }
} }

View File

@ -75,27 +75,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
} }
} }
/*
* Check for parameter combinations and names that are invalid
*/
List<IParameter> 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 * Only compartment searching methods may have an ID parameter
*/ */
@ -232,7 +211,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
} }
} }
for (String next : theRequest.getParameters().keySet()) { for (String next : theRequest.getParameters().keySet()) {
if (ALLOWED_PARAMS.contains(next)) { if (next.startsWith("_")) {
methodParamsTemp.add(next); methodParamsTemp.add(next);
} }
} }

View File

@ -11,6 +11,7 @@ import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import java.util.List; import java.util.List;
import java.util.Optional;
public class FluentPathDstu3 implements IFluentPath { public class FluentPathDstu3 implements IFluentPath {
@ -43,4 +44,9 @@ public class FluentPathDstu3 implements IFluentPath {
return (List<T>) result; return (List<T>) result;
} }
@Override
public <T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> theReturnType) {
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
}
} }

View File

@ -1,7 +1,8 @@
package org.hl7.fhir.r4.hapi.fluentpath; 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.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; 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.model.Base;
import org.hl7.fhir.r4.utils.FHIRPathEngine; import org.hl7.fhir.r4.utils.FHIRPathEngine;
import ca.uhn.fhir.context.FhirContext; import java.util.List;
import ca.uhn.fhir.fluentpath.FluentPathExecutionException; import java.util.Optional;
import ca.uhn.fhir.fluentpath.IFluentPath;
public class FluentPathR4 implements IFluentPath { public class FluentPathR4 implements IFluentPath {
private FHIRPathEngine myEngine; private FHIRPathEngine myEngine;
public FluentPathR4(FhirContext theCtx) { public FluentPathR4(FhirContext theCtx) {
if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { 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()); 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(); IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport();
myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport));
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public <T extends IBase> List<T> evaluate(IBase theInput, String thePath, Class<T> theReturnType) { public <T extends IBase> List<T> evaluate(IBase theInput, String thePath, Class<T> theReturnType) {
List<Base> result; List<Base> result;
try { try {
result = myEngine.evaluate((Base)theInput, thePath); result = myEngine.evaluate((Base) theInput, thePath);
} catch (FHIRException e) { } catch (FHIRException e) {
throw new FluentPathExecutionException(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<T>) result;
}
@Override
public <T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> 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<T>) result;
}
} }

View File

@ -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<? extends IBaseResource> 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";
}
}

View File

@ -132,6 +132,16 @@
An issue was corrected with the JPA reindexer, where String index columns do not always 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. get reindexed if they did not have an identity hash value in the HASH_IDENTITY column.
</action> </action>
<action type="add">
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.
</action>
<action type="add">
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.
</action>
</release> </release>
<release version="3.6.0" date="2018-11-12" description="Food"> <release version="3.6.0" date="2018-11-12" description="Food">
<action type="add"> <action type="add">