Add media interceptor
This commit is contained in:
parent
5a80e70d93
commit
b442982310
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue