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.Optional;
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);
/**
* 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;
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

View File

@ -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;

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() {
RestulfulServerConfiguration result = new RestulfulServerConfiguration();
result.setResourceBindings(getResourceBindings());
@ -1421,14 +1415,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
if (!newResourceProviders.isEmpty()) {
ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myResourceProviders.size());
for (IResourceProvider provider : newResourceProviders) {
assertProviderIsValid(provider);
findResourceMethods(provider);
}
}
if (!newPlainProviders.isEmpty()) {
ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size(), myPlainProviders.size());
for (Object provider : newPlainProviders) {
assertProviderIsValid(provider);
findResourceMethods(provider);
}
}

View File

@ -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 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) {
// 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<? extends IBaseResource> 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;
}
}
}

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) {

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.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;
@ -57,27 +56,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
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);
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 String myResourceName;
private Class<? extends IBaseResource> 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<? extends IResource>) 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<? 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;
}
for (String next : theRequest.getParameters().keySet()) {
if (!ALLOWED_PARAMS.contains(next)) {
if (!next.startsWith("_")) {
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
*/
@ -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);
}
}

View File

@ -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<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;
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,9 +10,8 @@ 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 {
@ -30,7 +30,7 @@ public class FluentPathR4 implements IFluentPath {
public <T extends IBase> List<T> evaluate(IBase theInput, String thePath, Class<T> theReturnType) {
List<Base> result;
try {
result = myEngine.evaluate((Base)theInput, thePath);
result = myEngine.evaluate((Base) theInput, thePath);
} catch (FHIRException e) {
throw new FluentPathExecutionException(e);
}
@ -44,4 +44,10 @@ public class FluentPathR4 implements IFluentPath {
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

@ -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
get reindexed if they did not have an identity hash value in the HASH_IDENTITY column.
</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 version="3.6.0" date="2018-11-12" description="Food">
<action type="add">