Add ability for resource source to be specified by header

This commit is contained in:
James Agnew 2019-10-23 21:02:07 -05:00
parent 0d3f7d4a03
commit 385a885abf
16 changed files with 807 additions and 434 deletions

View File

@ -28,6 +28,7 @@ public class Constants {
public static final String CT_TEXT_CSV = "text/csv"; public static final String CT_TEXT_CSV = "text/csv";
public static final String HEADER_REQUEST_ID = "X-Request-ID"; public static final String HEADER_REQUEST_ID = "X-Request-ID";
public static final String HEADER_REQUEST_SOURCE = "X-Request-Source";
public static final String CACHE_CONTROL_MAX_RESULTS = "max-results"; public static final String CACHE_CONTROL_MAX_RESULTS = "max-results";
public static final String CACHE_CONTROL_NO_CACHE = "no-cache"; public static final String CACHE_CONTROL_NO_CACHE = "no-cache";
public static final String CACHE_CONTROL_NO_STORE = "no-store"; public static final String CACHE_CONTROL_NO_STORE = "no-store";
@ -242,6 +243,15 @@ public class Constants {
* Operation name for the $lastn operation * Operation name for the $lastn operation
*/ */
public static final String OPERATION_LASTN = "$lastn"; public static final String OPERATION_LASTN = "$lastn";
/**
* <p>
* This extension represents the equivalent of the
* <code>Resource.meta.source</code> field within R4+ resources, and is for
* use in DSTU3 resources. It should contain a value of type <code>uri</code>
* and will be located on the Resource.meta
* </p>
*/
public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source";
static { static {
CHARSET_UTF8 = StandardCharsets.UTF_8; CHARSET_UTF8 = StandardCharsets.UTF_8;

View File

@ -23,9 +23,9 @@ package ca.uhn.fhir.util;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBase; import ca.uhn.fhir.context.FhirVersionEnum;
import org.hl7.fhir.instance.model.api.IBaseMetaType; import ca.uhn.fhir.rest.api.Constants;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.*;
import java.util.List; import java.util.List;
@ -46,6 +46,30 @@ public class MetaUtil {
return retVal; return retVal;
} }
/**
* Sets the value for <code>Resource.meta.source</code> for R4+ resources, and places the value in
* an extension on <code>Resource.meta</code>
* with the URL <code>http://hapifhir.io/fhir/StructureDefinition/resource-meta-source</code> for DSTU3
* and below.
*
* @param theContext The FhirContext object
* @param theResource The resource to modify
* @param theValue The source URI
* @see <a href="http://hl7.org/fhir/resource-definitions.html#Resource.meta">Meta.source</a>
*/
@SuppressWarnings("unchecked")
public static void setSource(FhirContext theContext, IBaseResource theResource, String theValue) {
if (theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
MetaUtil.setSource(theContext, theResource.getMeta(), theValue);
} else {
IBaseExtension<?, ?> sourceExtension = ((IBaseHasExtensions) theResource.getMeta()).addExtension();
sourceExtension.setUrl(Constants.EXT_META_SOURCE);
IPrimitiveType<String> value = (IPrimitiveType<String>) theContext.getElementDefinition("uri").newInstance();
value.setValue(theValue);
sourceExtension.setValue(value);
}
}
public static void setSource(FhirContext theContext, IBaseMetaType theMeta, String theValue) { public static void setSource(FhirContext theContext, IBaseMetaType theMeta, String theValue) {
BaseRuntimeElementCompositeDefinition<?> elementDef = (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theMeta.getClass()); BaseRuntimeElementCompositeDefinition<?> elementDef = (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theMeta.getClass());
BaseRuntimeChildDefinition sourceChild = elementDef.getChildByName("source"); BaseRuntimeChildDefinition sourceChild = elementDef.getChildByName("source");

View File

@ -25,7 +25,6 @@ import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.util.AddRemoveCount; import ca.uhn.fhir.jpa.util.AddRemoveCount;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
@ -900,15 +899,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
+ (isNotBlank(provenanceRequestId) ? "#" : "") + (isNotBlank(provenanceRequestId) ? "#" : "")
+ defaultString(provenanceRequestId); + defaultString(provenanceRequestId);
if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { MetaUtil.setSource(myContext, retVal, sourceString);
IBaseExtension<?, ?> sourceExtension = ((IBaseHasExtensions) retVal.getMeta()).addExtension();
sourceExtension.setUrl(JpaConstants.EXT_META_SOURCE);
IPrimitiveType<String> value = (IPrimitiveType<String>) myContext.getElementDefinition("uri").newInstance();
value.setValue(sourceString);
sourceExtension.setValue(value);
} else if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
MetaUtil.setSource(myContext, retVal.getMeta(), sourceString);
}
} }
return retVal; return retVal;
@ -1078,7 +1070,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
source = ((IBaseHasExtensions) theResource.getMeta()) source = ((IBaseHasExtensions) theResource.getMeta())
.getExtension() .getExtension()
.stream() .stream()
.filter(t -> JpaConstants.EXT_META_SOURCE.equals(t.getUrl())) .filter(t -> Constants.EXT_META_SOURCE.equals(t.getUrl()))
.filter(t -> t.getValue() instanceof IPrimitiveType) .filter(t -> t.getValue() instanceof IPrimitiveType)
.map(t -> ((IPrimitiveType) t.getValue()).getValueAsString()) .map(t -> ((IPrimitiveType) t.getValue()).getValueAsString())
.findFirst() .findFirst()

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.jpa.dao.dstu3; package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
@ -46,12 +45,12 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
when(mySrd.getRequestId()).thenReturn(requestId); when(mySrd.getRequestId()).thenReturn(requestId);
Patient pt0 = new Patient(); Patient pt0 = new Patient();
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:0"));
pt0.setActive(true); pt0.setActive(true);
IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless();
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); pt1.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:1"));
pt1.setActive(true); pt1.setActive(true);
IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
@ -62,7 +61,7 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
IBundleProvider result = myPatientDao.search(params); IBundleProvider result = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue())); assertThat(toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(pt0id.getValue()));
pt0 = (Patient) result.getResources(0, 1).get(0); pt0 = (Patient) result.getResources(0, 1).get(0);
assertEquals("urn:source:0#a_request_id", pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); assertEquals("urn:source:0#a_request_id", pt0.getMeta().getExtensionString(Constants.EXT_META_SOURCE));
// Search by request ID // Search by request ID
params = new SearchParameterMap(); params = new SearchParameterMap();
@ -87,17 +86,17 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
when(mySrd.getRequestId()).thenReturn(requestId); when(mySrd.getRequestId()).thenReturn(requestId);
Patient pt0 = new Patient(); Patient pt0 = new Patient();
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:0"));
pt0.setActive(true); pt0.setActive(true);
IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless();
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); pt1.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:1"));
pt1.setActive(true); pt1.setActive(true);
IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
Patient pt2 = new Patient(); Patient pt2 = new Patient();
pt2.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:2")); pt2.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:2"));
pt2.setActive(true); pt2.setActive(true);
myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless(); myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless();
@ -118,17 +117,17 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
when(mySrd.getRequestId()).thenReturn(requestId); when(mySrd.getRequestId()).thenReturn(requestId);
Patient pt0 = new Patient(); Patient pt0 = new Patient();
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:0"));
pt0.setActive(true); pt0.setActive(true);
IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless();
Patient pt1 = new Patient(); Patient pt1 = new Patient();
pt1.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); pt1.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:1"));
pt1.setActive(true); pt1.setActive(true);
IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless(); IIdType pt1id = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
Patient pt2 = new Patient(); Patient pt2 = new Patient();
pt2.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:2")); pt2.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:2"));
pt2.setActive(true); pt2.setActive(true);
myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless(); myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless();
@ -148,7 +147,7 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
when(mySrd.getRequestId()).thenReturn(requestId); when(mySrd.getRequestId()).thenReturn(requestId);
Patient pt0 = new Patient(); Patient pt0 = new Patient();
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:0"));
pt0.setActive(true); pt0.setActive(true);
IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless();
@ -166,19 +165,19 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
public void testSourceNotPreservedAcrossUpdate() { public void testSourceNotPreservedAcrossUpdate() {
Patient pt0 = new Patient(); Patient pt0 = new Patient();
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:0"));
pt0.setActive(true); pt0.setActive(true);
IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless();
pt0 = myPatientDao.read(pt0id); pt0 = myPatientDao.read(pt0id);
assertEquals("urn:source:0", pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); assertEquals("urn:source:0", pt0.getMeta().getExtensionString(Constants.EXT_META_SOURCE));
pt0.getMeta().getExtension().clear(); pt0.getMeta().getExtension().clear();
pt0.setActive(false); pt0.setActive(false);
myPatientDao.update(pt0); myPatientDao.update(pt0);
pt0 = myPatientDao.read(pt0id.withVersion("2")); pt0 = myPatientDao.read(pt0id.withVersion("2"));
assertEquals(null, pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); assertEquals(null, pt0.getMeta().getExtensionString(Constants.EXT_META_SOURCE));
} }
@ -188,19 +187,19 @@ public class FhirResourceDaoDstu3SourceTest extends BaseJpaDstu3Test {
when(mySrd.getRequestId()).thenReturn("0000000000000000"); when(mySrd.getRequestId()).thenReturn("0000000000000000");
Patient pt0 = new Patient(); Patient pt0 = new Patient();
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:0")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:0"));
pt0.setActive(true); pt0.setActive(true);
IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless(); IIdType pt0id = myPatientDao.create(pt0, mySrd).getId().toUnqualifiedVersionless();
pt0 = myPatientDao.read(pt0id); pt0 = myPatientDao.read(pt0id);
assertEquals(null, pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); assertEquals(null, pt0.getMeta().getExtensionString(Constants.EXT_META_SOURCE));
pt0.getMeta().addExtension(JpaConstants.EXT_META_SOURCE, new StringType("urn:source:1")); pt0.getMeta().addExtension(Constants.EXT_META_SOURCE, new StringType("urn:source:1"));
pt0.setActive(false); pt0.setActive(false);
myPatientDao.update(pt0); myPatientDao.update(pt0);
pt0 = myPatientDao.read(pt0id.withVersion("2")); pt0 = myPatientDao.read(pt0id.withVersion("2"));
assertEquals(null, pt0.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE)); assertEquals(null, pt0.getMeta().getExtensionString(Constants.EXT_META_SOURCE));
// Search without source param // Search without source param
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.r4.ResourceProviderR4Test; import ca.uhn.fhir.jpa.provider.r4.ResourceProviderR4Test;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
@ -3952,7 +3951,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
{ {
Patient readPatient = (Patient) ourClient.read().resource("Patient").withId(patientid).execute(); Patient readPatient = (Patient) ourClient.read().resource("Patient").withId(patientid).execute();
assertThat(readPatient.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE), matchesPattern("#[a-zA-Z0-9]+")); assertThat(readPatient.getMeta().getExtensionString(Constants.EXT_META_SOURCE), matchesPattern("#[a-zA-Z0-9]+"));
} }
patient.setId(patientid); patient.setId(patientid);
@ -3960,12 +3959,12 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
ourClient.update().resource(patient).execute(); ourClient.update().resource(patient).execute();
{ {
Patient readPatient = (Patient) ourClient.read().resource("Patient").withId(patientid).execute(); Patient readPatient = (Patient) ourClient.read().resource("Patient").withId(patientid).execute();
assertThat(readPatient.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE), matchesPattern("#[a-zA-Z0-9]+")); assertThat(readPatient.getMeta().getExtensionString(Constants.EXT_META_SOURCE), matchesPattern("#[a-zA-Z0-9]+"));
readPatient.addName().setFamily("testUpdateWithSource"); readPatient.addName().setFamily("testUpdateWithSource");
ourClient.update().resource(readPatient).execute(); ourClient.update().resource(readPatient).execute();
readPatient = (Patient) ourClient.read().resource("Patient").withId(patientid).execute(); readPatient = (Patient) ourClient.read().resource("Patient").withId(patientid).execute();
assertThat(readPatient.getMeta().getExtensionString(JpaConstants.EXT_META_SOURCE), matchesPattern("#[a-zA-Z0-9]+")); assertThat(readPatient.getMeta().getExtensionString(Constants.EXT_META_SOURCE), matchesPattern("#[a-zA-Z0-9]+"));
} }
} }

View File

@ -243,16 +243,6 @@ public class JpaConstants {
*/ */
public static final String EXTENSION_EXT_SYSTEMDEFINED = JpaConstants.class.getName() + "_EXTENSION_EXT_SYSTEMDEFINED"; public static final String EXTENSION_EXT_SYSTEMDEFINED = JpaConstants.class.getName() + "_EXTENSION_EXT_SYSTEMDEFINED";
/**
* <p>
* This extension represents the equivalent of the
* <code>Resource.meta.source</code> field within R4+ resources, and is for
* use in DSTU3 resources. It should contain a value of type <code>uri</code>
* and will be located on the Resource.meta
* </p>
*/
public static final String EXT_META_SOURCE = "http://hapifhir.io/fhir/StructureDefinition/resource-meta-source";
/** /**
* Parameter for the $export operation * Parameter for the $export operation
*/ */

View File

@ -370,7 +370,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e); throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e);
} }
if (count == 0) { if (count == 0) {
throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName()); throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getName());
} }
} }

View File

@ -0,0 +1,66 @@
package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.MetaUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This interceptor examines a header on the incoming request and places it in
* <code>Resource.meta.source</code> (R4 and above) or in an extension on <code>Resource.meta</code>
* with the URL <code>http://hapifhir.io/fhir/StructureDefinition/resource-meta-source</code> (DSTU3 and below).
* <p>
* This interceptor does not support versions of FHIR below DSTU3.
* </p>
*
* @see <a href="http://hl7.org/fhir/resource-definitions.html#Resource.meta">Meta.source</a>
*/
@Interceptor
public class CaptureResourceSourceFromHeaderInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(CaptureResourceSourceFromHeaderInterceptor.class);
private final FhirContext myFhirContext;
private String myHeaderName;
public CaptureResourceSourceFromHeaderInterceptor(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
setHeaderName(Constants.HEADER_REQUEST_SOURCE);
}
/**
* Provides the header name to examine in incoming requests. Default is {@link ca.uhn.fhir.rest.api.Constants#HEADER_REQUEST_SOURCE "X-Request-Source"}.
*/
@SuppressWarnings("WeakerAccess")
public String getHeaderName() {
return myHeaderName;
}
/**
* Provides the header name to examine in incoming requests. Default is {@link ca.uhn.fhir.rest.api.Constants#HEADER_REQUEST_SOURCE "X-Request-Source"}.
*/
@SuppressWarnings("WeakerAccess")
public void setHeaderName(String theHeaderName) {
myHeaderName = theHeaderName;
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void extractSource(RequestDetails theRequestDetails) {
IBaseResource resource = theRequestDetails.getResource();
if (resource != null) {
String requestSource = theRequestDetails.getHeader(getHeaderName());
if (isNotBlank(requestSource)) {
ourLog.trace("Setting Meta.source to \"{}\" because of header \"{}\"", requestSource, getHeaderName());
MetaUtil.setSource(myFhirContext, resource, requestSource);
}
}
}
}

View File

@ -126,6 +126,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
return new MethodOutcome() return new MethodOutcome()
.setCreated(true) .setCreated(true)
.setResource(theResource)
.setId(theResource.getIdElement()); .setId(theResource.getIdElement());
} }
@ -373,6 +374,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
return new MethodOutcome() return new MethodOutcome()
.setCreated(created) .setCreated(created)
.setResource(theResource)
.setId(theResource.getIdElement()); .setId(theResource.getIdElement());
} }
@ -416,6 +418,19 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
return theResource.getIdElement(); return theResource.getIdElement();
} }
/**
* Returns an unmodifiable list containing the current version of all resources stored in this provider
*
* @since 4.1.0
*/
public List<T> getStoredResources() {
List<T> retVal = new ArrayList<>();
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
retVal.add(next.lastEntry().getValue());
}
return Collections.unmodifiableList(retVal);
}
private static <T extends IBaseResource> T fireInterceptorsAndFilterAsNeeded(T theResource, RequestDetails theRequestDetails) { private static <T extends IBaseResource> T fireInterceptorsAndFilterAsNeeded(T theResource, RequestDetails theRequestDetails) {
List<T> output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails); List<T> output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails);
if (output.size() == 1) { if (output.size() == 1) {

View File

@ -87,9 +87,8 @@ public class SearchR4Test {
* A paging request that incorrectly executes at the type level shouldn't be grabbed by the search method binding * A paging request that incorrectly executes at the type level shouldn't be grabbed by the search method binding
*/ */
@Test @Test
public void tesPageRequestCantTriggerSearchAccidentally() throws Exception { public void testPageRequestCantTriggerSearchAccidentally() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Constants.PARAM_PAGINGACTION + "=12345"); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?" + Constants.PARAM_PAGINGACTION + "=12345");
Bundle bundle;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) { try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent); ourLog.info(responseContent);

View File

@ -0,0 +1,102 @@
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.server.provider.HashMapResourceProvider;
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderRule;
import ca.uhn.fhir.test.utilities.server.RestfulServerRule;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.junit.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class CaptureResourceSourceFromHeaderInterceptorTest {
private static FhirContext ourCtx = FhirContext.forR4();
@ClassRule
public static RestfulServerRule ourServerRule = new RestfulServerRule(ourCtx);
private CaptureResourceSourceFromHeaderInterceptor myInterceptor;
@Rule
public HashMapResourceProviderRule<Patient> myPatientProviderRule = new HashMapResourceProviderRule<>(ourServerRule, Patient.class);
@Before
public void before() {
myInterceptor = new CaptureResourceSourceFromHeaderInterceptor(ourCtx);
ourServerRule.getRestfulServer().registerInterceptor(myInterceptor);
}
@After
public void after() {
ourServerRule.getRestfulServer().unregisterInterceptor(myInterceptor);
}
@Test
public void testCreateWithoutAnything() {
Patient resource = new Patient();
resource.setActive(true);
ourServerRule.getFhirClient().create().resource(resource).execute();
Patient stored = myPatientProviderRule.getStoredResources().get(0);
assertNull(stored.getMeta().getSource());
}
@Test
public void testCreateWithSource() {
Patient resource = new Patient();
resource.setActive(true);
resource.getMeta().setSource("http://source");
ourServerRule.getFhirClient().create().resource(resource).execute();
Patient stored = myPatientProviderRule.getStoredResources().get(0);
assertEquals("http://source", stored.getMeta().getSource());
}
@Test
public void testCreateWithHeader() {
Patient resource = new Patient();
resource.setActive(true);
ourServerRule
.getFhirClient()
.create()
.resource(resource)
.withAdditionalHeader(Constants.HEADER_REQUEST_SOURCE, "http://header")
.execute();
Patient stored = myPatientProviderRule.getStoredResources().get(0);
assertEquals("http://header", stored.getMeta().getSource());
}
@Test
public void testCreateWithBoth() {
Patient resource = new Patient();
resource.setActive(true);
resource.getMeta().setSource("http://source");
ourServerRule
.getFhirClient()
.create()
.resource(resource)
.withAdditionalHeader(Constants.HEADER_REQUEST_SOURCE, "http://header")
.execute();
Patient stored = myPatientProviderRule.getStoredResources().get(0);
assertEquals("http://header", stored.getMeta().getSource());
}
@Test
public void testNonCreateShouldntFail() {
Bundle bundle = ourServerRule
.getFhirClient()
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.execute();
assertEquals(0, bundle.getEntry().size());
}
}

View File

@ -21,6 +21,16 @@
<artifactId>hapi-fhir-base</artifactId> <artifactId>hapi-fhir-base</artifactId>
<version>4.1.0-SNAPSHOT</version> <version>4.1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId>
<version>4.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>4.1.0-SNAPSHOT</version>
</dependency>
<!-- General --> <!-- General -->
<dependency> <dependency>
@ -53,6 +63,16 @@
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
</dependency> </dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -0,0 +1,42 @@
package ca.uhn.fhir.test.utilities.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class HashMapResourceProviderRule<T extends IBaseResource> extends HashMapResourceProvider<T> implements TestRule {
private final RestfulServerRule myRestfulServerRule;
/**
* Constructor
*
* @param theFhirContext The FHIR context
* @param theResourceType The resource type to support
*/
public HashMapResourceProviderRule(RestfulServerRule theRestfulServerRule, Class<T> theResourceType) {
super(theRestfulServerRule.getFhirContext(), theResourceType);
myRestfulServerRule = theRestfulServerRule;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
clear();
myRestfulServerRule.getRestfulServer().registerProvider(HashMapResourceProviderRule.this);
try {
base.evaluate();
} finally {
myRestfulServerRule.getRestfulServer().unregisterProvider(HashMapResourceProviderRule.this);
}
}
};
}
}

View File

@ -0,0 +1,95 @@
package ca.uhn.fhir.test.utilities.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.test.utilities.JettyUtil;
import org.apache.commons.lang3.time.DateUtils;
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.junit.BeforeClass;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
public class RestfulServerRule implements TestRule {
private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerRule.class);
private final FhirContext myFhirContext;
private final Object[] myProviders;
private Server myServer;
private RestfulServer myServlet;
private int myPort;
private CloseableHttpClient myHttpClient;
private IGenericClient myFhirClient;
public RestfulServerRule(FhirContext theFhirContext, Object... theProviders) {
myFhirContext = theFhirContext;
myProviders = theProviders;
}
@Override
public Statement apply(Statement theBase, Description theDescription) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
startServer();
theBase.evaluate();
stopServer();
}
};
}
private void stopServer() throws Exception {
JettyUtil.closeServer(myServer);
myHttpClient.close();
}
@BeforeClass
private void startServer() throws Exception {
myServer = new Server(0);
ServletHandler servletHandler = new ServletHandler();
myServlet = new RestfulServer(myFhirContext);
myServlet.setDefaultPrettyPrint(true);
myServlet.registerProviders(myProviders);
ServletHolder servletHolder = new ServletHolder(myServlet);
servletHandler.addServletWithMapping(servletHolder, "/*");
myServer.setHandler(servletHandler);
myServer.start();
myPort = JettyUtil.getPortForStartedServer(myServer);
ourLog.info("Server has started on port {}", myPort);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
myHttpClient = builder.build();
myFhirContext.getRestfulClientFactory().setSocketTimeout((int) (500 * DateUtils.MILLIS_PER_SECOND));
myFhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
myFhirClient = myFhirContext.newRestfulGenericClient("http://localhost:" + myPort);
}
public IGenericClient getFhirClient() {
return myFhirClient;
}
public FhirContext getFhirContext() {
return myFhirContext;
}
public RestfulServer getRestfulServer() {
return myServlet;
}
}

View File

@ -450,6 +450,14 @@
Several issues with HAPI FHIR's annotation scanner that prevented use with Kotlin based Several issues with HAPI FHIR's annotation scanner that prevented use with Kotlin based
resource providers have been corrected. Thanks to Jelmer ter Wal for the pull request! resource providers have been corrected. Thanks to Jelmer ter Wal for the pull request!
</action> </action>
<action type="add">
A new built-in server interceptor called
<![CDATA[<code>CaptureResourceSourceFromHeaderInterceptor</code>]]>
has been added.
This interceptor can be used to capture an incoming source system URI in an HTTP Request
Header and automatically place it in
<![CDATA[<code>Resource.meta.source</code>]]>
</action>
</release> </release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)"> <release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix"> <action type="fix">

View File

@ -1,385 +1,397 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd"> <document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties> <properties>
<title>Server Interceptors</title> <title>Server Interceptors</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author> <author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties> </properties>
<body> <body>
<!-- The body of the document contains a number of sections --> <!-- The body of the document contains a number of sections -->
<section name="Server Interceptors"> <section name="Server Interceptors">
<p> <p>
The RESTful server provides a powerful mechanism for adding cross-cutting behaviour The RESTful server provides a powerful mechanism for adding cross-cutting behaviour
(e.g. requests, such as authnorization, auditing, fancy output, logging, etc.) (e.g. requests, such as authnorization, auditing, fancy output, logging, etc.)
to each incoming request that it processes. This mechanism consists of defining one or to each incoming request that it processes. This mechanism consists of defining one or
more <b>interceptors</b> that will be invoked at defined points in the processing of more <b>interceptors</b> that will be invoked at defined points in the processing of
each incoming request. each incoming request.
</p> </p>
<img src="svg/restful-server-interceptors.svg" alt="Interceptors"/> <img src="svg/restful-server-interceptors.svg" alt="Interceptors"/>
<p> <p>
Interceptors will intercept the incoming request, and can take action such as Interceptors will intercept the incoming request, and can take action such as
logging or auditing it, or examining/injecting headers. They can optionally choose logging or auditing it, or examining/injecting headers. They can optionally choose
to handle the request directly and the cancel any subsequent processing (in other words, to handle the request directly and the cancel any subsequent processing (in other words,
the interceptor can choose to supply a response to the client, and can then signal the interceptor can choose to supply a response to the client, and can then signal
to the server that it does not need to do so). to the server that it does not need to do so).
</p> </p>
<p> <p>
Interceptors Interceptors
may also be notified of responses prior to those responses being served to a client, may also be notified of responses prior to those responses being served to a client,
and may audit or even cancel the response. The diagram on the right shows the and may audit or even cancel the response. The diagram on the right shows the
lifecycle of a normal (non failing) request which is subject to an interceptor. lifecycle of a normal (non failing) request which is subject to an interceptor.
</p> </p>
<p> <p>
Interceptors must implement the Interceptors must implement the
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.html">IServerInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.html">IServerInterceptor</a>
interface (or extend the convenience interface (or extend the convenience
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.html">InterceptorAdapter</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.html">InterceptorAdapter</a>
class provided). The RESTful server will normally invoke the interceptor at several class provided). The RESTful server will normally invoke the interceptor at several
points in the execution of the client request. points in the execution of the client request.
</p> </p>
<ul> <ul>
<li> <li>
Before any processing at all is performed on the request, Before any processing at all is performed on the request,
<b>incomingRequestPreProcessed</b> will be invoked. This can be useful <b>incomingRequestPreProcessed</b> will be invoked. This can be useful
if you wish to handle some requests completely outside of HAPI's processing if you wish to handle some requests completely outside of HAPI's processing
mechanism. mechanism.
<ul> <ul>
<li> <li>
If this method returns <code>true</code>, processing continues to the If this method returns <code>true</code>, processing continues to the
next interceptor, and ultimately to the next phase of processing. next interceptor, and ultimately to the next phase of processing.
</li> </li>
<li> <li>
If this method returns <code>false</code>, processing stops immediately. If this method returns <code>false</code>, processing stops immediately.
This is useful if the interceptor wishes to supply its own response This is useful if the interceptor wishes to supply its own response
by directly calling methods on the <code>HttpServletResponse</code> by directly calling methods on the <code>HttpServletResponse</code>
</li> </li>
<li> <li>
If this method throws any subclass of If this method throws any subclass of
<a href="./apidocs/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.html">BaseServerResponseException</a>, <a href="./apidocs/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.html">BaseServerResponseException</a>,
processing is stopped immedicately and the corresponding status is returned to the client. processing is stopped immedicately and the corresponding status is returned to the client.
This is useful if an interceptor wishes to abort the request (e.g. because This is useful if an interceptor wishes to abort the request (e.g. because
it did not detect valid credentials) it did not detect valid credentials)
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
Once the request is classified (meaning that the URL and request headers are Once the request is classified (meaning that the URL and request headers are
examined to determine exactly what kind of request is being made), examined to determine exactly what kind of request is being made),
<b>incomingRequestPostProcessed</b> will be invoked. This method has <b>incomingRequestPostProcessed</b> will be invoked. This method has
an additional parameter, the an additional parameter, the
<a href="./apidocs/ca/uhn/fhir/rest/method/RequestDetails.html">RequestDetails</a> <a href="./apidocs/ca/uhn/fhir/rest/method/RequestDetails.html">RequestDetails</a>
object which contains details about what operation is about to be object which contains details about what operation is about to be
called, and what request parameters were receievd with that request. called, and what request parameters were receievd with that request.
<ul> <ul>
<li> <li>
If this method returns <code>true</code>, processing continues to the If this method returns <code>true</code>, processing continues to the
next interceptor, and ultimately to the next phase of processing. next interceptor, and ultimately to the next phase of processing.
</li> </li>
<li> <li>
If this method returns <code>false</code>, processing stops immediately. If this method returns <code>false</code>, processing stops immediately.
This is useful if the interceptor wishes to supply its own response This is useful if the interceptor wishes to supply its own response
by directly calling methods on the <code>HttpServletResponse</code> by directly calling methods on the <code>HttpServletResponse</code>
</li> </li>
<li> <li>
If this method throws any subclass of If this method throws any subclass of
<a href="./apidocs/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.html">BaseServerResponseException</a>, <a href="./apidocs/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.html">BaseServerResponseException</a>,
processing is stopped immedicately and the corresponding status is returned to the client. processing is stopped immedicately and the corresponding status is returned to the client.
This is useful if an interceptor wishes to abort the request (e.g. because This is useful if an interceptor wishes to abort the request (e.g. because
it did not detect valid credentials) it did not detect valid credentials)
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
Once the request is being handled, Once the request is being handled,
<b>incomingRequestPreHandled</b> will be invoked. This method is useful in that <b>incomingRequestPreHandled</b> will be invoked. This method is useful in that
it provides details about the FHIR operation being invoked (e.g. is this a "read" or a "create"? what it provides details about the FHIR operation being invoked (e.g. is this a "read" or a "create"? what
is the resource type and ID of the resource being accessed, etc.). This method can be is the resource type and ID of the resource being accessed, etc.). This method can be
useful for adding finer grained access controls. Note that <code>incomingRequestPreHandled</code> useful for adding finer grained access controls. Note that <code>incomingRequestPreHandled</code>
is not able to directly supply a response, but it may throw a is not able to directly supply a response, but it may throw a
<a href="./apidocs/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.html">BaseServerResponseException</a> <a href="./apidocs/ca/uhn/fhir/rest/server/exceptions/BaseServerResponseException.html">BaseServerResponseException</a>
to abort processing. to abort processing.
</li> </li>
<li> <li>
After the operation is handled (by invoking the corresponding ResourceProvider or PlainProvider method), After the operation is handled (by invoking the corresponding ResourceProvider or PlainProvider method),
but before the actual response is returned to the client, but before the actual response is returned to the client,
the <b>outgoingResponse</b> method is invoked. the <b>outgoingResponse</b> method is invoked.
This method also has details about the request in its parameters, but also This method also has details about the request in its parameters, but also
receives a copy of the response that is about to be returned. Note that receives a copy of the response that is about to be returned. Note that
there are three implementations of <b>outgoingResponse</b>: The server there are three implementations of <b>outgoingResponse</b>: The server
will invoke the one which corresponds to the response type will invoke the one which corresponds to the response type
of the operation being invoked (resource, bundle, etc.) of the operation being invoked (resource, bundle, etc.)
</li> </li>
</ul> </ul>
<br clear="all"/> <br clear="all"/>
<subsection name="Exception Handling"> <subsection name="Exception Handling">
<img src="svg/restful-server-interceptors-exception.svg" alt="Interceptors" align="right"/> <img src="svg/restful-server-interceptors-exception.svg" alt="Interceptors" align="right"/>
<p> <p>
In the event of an exception being thrown within the server, the interceptor In the event of an exception being thrown within the server, the interceptor
method method
<code><a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.html#handleException(ca.uhn.fhir.rest.method.RequestDetails,%20java.lang.Throwable,%20javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse)">handleException</a></code> <code><a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.html#handleException(ca.uhn.fhir.rest.method.RequestDetails,%20java.lang.Throwable,%20javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse)">handleException</a></code>
will be called. This applies both to HAPI-FHIR defined exceptions thrown within resource provider methods will be called. This applies both to HAPI-FHIR defined exceptions thrown within resource provider methods
you have created as well as unexpected exceptions such as NullPointerException thrown you have created as well as unexpected exceptions such as NullPointerException thrown
at any point in the handling chain. at any point in the handling chain.
</p> </p>
<p> <p>
In general, you will want to return <code>true</code> from the <code>handleException</code> In general, you will want to return <code>true</code> from the <code>handleException</code>
method, which means that processing continues normally (RestfulServer will return an method, which means that processing continues normally (RestfulServer will return an
HTTP 4xx or 5xx response automatically depending on the specific exception which was thrown). HTTP 4xx or 5xx response automatically depending on the specific exception which was thrown).
</p> </p>
<p> <p>
However, you may override the server's built-in exception handling by returning However, you may override the server's built-in exception handling by returning
<code>false</code>. In this case, you must provide your own response by <code>false</code>. In this case, you must provide your own response by
interacting with the <code>HttpServletResponse</code> object which is interacting with the <code>HttpServletResponse</code> object which is
passed in. passed in.
</p> </p>
</subsection> </subsection>
<br clear="all"/> <br clear="all"/>
<subsection name="Registering Interceptors"> <subsection name="Registering Interceptors">
<p> <p>
To register an interceptor to the server, simply call To register an interceptor to the server, simply call
either <code>registerInterceptor</code> or <code>setInterceptors</code> either <code>registerInterceptor</code> or <code>setInterceptors</code>
on your RestfulServer instance. on your RestfulServer instance.
</p> </p>
<p> <p>
Note that order is important: The server will invoke Note that order is important: The server will invoke
<code>incomingRequestPreProcessed</code> and <code>incomingRequestPostProcessed</code> <code>incomingRequestPreProcessed</code> and <code>incomingRequestPostProcessed</code>
in the same order that they are registered to the server. The server will in the same order that they are registered to the server. The server will
invoke <code>outgoingResponse</code> in the <b>reverse</b> order to the invoke <code>outgoingResponse</code> in the <b>reverse</b> order to the
order in which the interceptors were registered. This means that interceptors order in which the interceptors were registered. This means that interceptors
can be thought of as "wrapping" the request. can be thought of as "wrapping" the request.
</p> </p>
</subsection> </subsection>
</section> </section>
<section name="Built In Interceptors"> <section name="Built In Interceptors">
<p> <p>
HAPI also provides built-in interceptors which may be useful. Links to the code for each interceptor HAPI also provides built-in interceptors which may be useful. Links to the code for each interceptor
is also provided, to give examples of how interceptors are written. is also provided, to give examples of how interceptors are written.
</p> </p>
<a name="Logging"/> <a name="Logging"/>
<subsection name="Logging Server Requests"> <subsection name="Logging Server Requests">
<p> <p>
The The
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.html">LoggingInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.html">LoggingInterceptor</a>
(<a href="./xref/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.html">code</a>) (<a href="./xref/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.html">code</a>)
can be used to generate a new log line (via SLF4j) for each incoming request. LoggingInterceptor can be used to generate a new log line (via SLF4j) for each incoming request. LoggingInterceptor
provides a flexible message format that can be used to provide a customized level provides a flexible message format that can be used to provide a customized level
of detail about each incoming request. of detail about each incoming request.
</p> </p>
<p> <p>
The following example shows how to register a logging interceptor within The following example shows how to register a logging interceptor within
a FHIR RESTful server. a FHIR RESTful server.
</p> </p>
<macro name="snippet"> <macro name="snippet">
<param name="id" value="loggingInterceptor" /> <param name="id" value="loggingInterceptor" />
<param name="file" value="examples/src/main/java/example/ServletExamples.java" /> <param name="file" value="examples/src/main/java/example/ServletExamples.java" />
</macro> </macro>
<p> <p>
This interceptor will then produce output similar to the following: This interceptor will then produce output similar to the following:
</p> </p>
<source><![CDATA[2014-09-04 02:37:30.030 Source[127.0.0.1] Operation[vread Patient/1667/_history/1] UA[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 Safari/537.36] Params[?_format=json] <source><![CDATA[2014-09-04 02:37:30.030 Source[127.0.0.1] Operation[vread Patient/1667/_history/1] UA[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 Safari/537.36] Params[?_format=json]
2014-09-04 03:30:00.443 Source[127.0.0.1] Operation[search-type Organization] UA[Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)] Params[]]]></source> 2014-09-04 03:30:00.443 Source[127.0.0.1] Operation[search-type Organization] UA[Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)] Params[]]]></source>
</subsection> </subsection>
<a name="ExceptionHandlingInterceptor"/> <a name="ExceptionHandlingInterceptor"/>
<subsection name="Exception Handling"> <subsection name="Exception Handling">
<p> <p>
The The
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.html">ExceptionHandlingInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.html">ExceptionHandlingInterceptor</a>
(<a href="./xref/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.html">code</a>) (<a href="./xref/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.html">code</a>)
can be used to customize what is returned to the client and what is logged when the server throws an can be used to customize what is returned to the client and what is logged when the server throws an
exception for any reason (including routine things like UnprocessableEntityExceptions thrown as a matter of exception for any reason (including routine things like UnprocessableEntityExceptions thrown as a matter of
normal processing in a create method, but also including unexpected NullPointerExceptions thrown by client code). normal processing in a create method, but also including unexpected NullPointerExceptions thrown by client code).
</p> </p>
<p> <p>
The following example shows how to register an exception handling interceptor within The following example shows how to register an exception handling interceptor within
a FHIR RESTful server. a FHIR RESTful server.
</p> </p>
<macro name="snippet"> <macro name="snippet">
<param name="id" value="exceptionInterceptor" /> <param name="id" value="exceptionInterceptor" />
<param name="file" value="examples/src/main/java/example/ServletExamples.java" /> <param name="file" value="examples/src/main/java/example/ServletExamples.java" />
</macro> </macro>
</subsection> </subsection>
<subsection name="Response Syntax Highlighting"> <subsection name="Response Syntax Highlighting">
<p> <p>
The The
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.html">ResponseHighlighterInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.html">ResponseHighlighterInterceptor</a>
(<a href="./xref/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.html">code</a>) (<a href="./xref/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.html">code</a>)
detects when a request is coming from a browser and returns HTML with syntax highlighted XML/JSON instead detects when a request is coming from a browser and returns HTML with syntax highlighted XML/JSON instead
of just the raw text. In other words, if a user uses a browser to request "http://foo/Patient/1" by typing of just the raw text. In other words, if a user uses a browser to request "http://foo/Patient/1" by typing
this address into their URL bar, they will get nice formatted HTML back with a human readable version this address into their URL bar, they will get nice formatted HTML back with a human readable version
of the content. This is helpful for testers. of the content. This is helpful for testers.
</p> </p>
<p> <p>
To see an example of how this looks, see our demo server using the following example To see an example of how this looks, see our demo server using the following example
query: query:
<a href="http://fhirtest.uhn.ca/baseDstu2/Patient/">http://fhirtest.uhn.ca/baseDstu2/Patient</a> <a href="http://fhirtest.uhn.ca/baseDstu2/Patient/">http://fhirtest.uhn.ca/baseDstu2/Patient</a>
</p> </p>
<p> <p>
The following example shows how to register this interceptor within The following example shows how to register this interceptor within
a FHIR RESTful server. a FHIR RESTful server.
</p> </p>
<macro name="snippet"> <macro name="snippet">
<param name="id" value="responseHighlighterInterceptor" /> <param name="id" value="responseHighlighterInterceptor" />
<param name="file" value="examples/src/main/java/example/ServletExamples.java" /> <param name="file" value="examples/src/main/java/example/ServletExamples.java" />
</macro> </macro>
</subsection> </subsection>
<subsection name="Request/Response Validation"> <subsection name="Request/Response Validation">
<p> <p>
The The
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/RequestValidatingInterceptor.html">RequestValidatingInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/RequestValidatingInterceptor.html">RequestValidatingInterceptor</a>
and and
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ResponseValidatingInterceptor.html">ResponseValidatingInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/ResponseValidatingInterceptor.html">ResponseValidatingInterceptor</a>
can be used to perform validation of resources on their way into and out of the server respectively. can be used to perform validation of resources on their way into and out of the server respectively.
</p> </p>
<p> <p>
The RequestValidatingInterceptor looks at resources coming into the server (e.g. for create, The RequestValidatingInterceptor looks at resources coming into the server (e.g. for create,
update, $operations, transactions, etc.) and validates them. The ResponseValidatingInterceptor update, $operations, transactions, etc.) and validates them. The ResponseValidatingInterceptor
looks at resources being returned by the server (e.g. for read, search, $operations, etc.) and looks at resources being returned by the server (e.g. for read, search, $operations, etc.) and
validates them. validates them.
</p> </p>
<p> <p>
These interceptors can be configured to add headers to the response, fail the response These interceptors can be configured to add headers to the response, fail the response
(returning an HTTP 422 and throwing an exception in the process), or to add to the (returning an HTTP 422 and throwing an exception in the process), or to add to the
OperationOutcome returned by the server. OperationOutcome returned by the server.
</p> </p>
<p> <p>
See the <a href="./doc_validation.html">Validation Page</a> for information on See the <a href="./doc_validation.html">Validation Page</a> for information on
available available
<a href="./apidocs/ca/uhn/fhir/validation/IValidatorModule.html">IValidatorModule</a> <a href="./apidocs/ca/uhn/fhir/validation/IValidatorModule.html">IValidatorModule</a>
validation modules. Any of the <b>Resource Validators</b> validation modules. Any of the <b>Resource Validators</b>
listed on that page can be enabled in these interceptors (note that the <b>Parser Validators</b> listed on that page can be enabled in these interceptors (note that the <b>Parser Validators</b>
can not). can not).
</p> </p>
<p> <p>
The following example shows how to register this interceptor within The following example shows how to register this interceptor within
a FHIR RESTful server. a FHIR RESTful server.
</p> </p>
<macro name="snippet"> <macro name="snippet">
<param name="id" value="validatingInterceptor" /> <param name="id" value="validatingInterceptor" />
<param name="file" value="examples/src/main/java/example/ServletExamples.java" /> <param name="file" value="examples/src/main/java/example/ServletExamples.java" />
</macro> </macro>
</subsection> </subsection>
<subsection name="CORS (Cross-Origin Resource Sharing)"> <subsection name="CORS (Cross-Origin Resource Sharing)">
<p> <p>
HAPI FHIR includes an interceptor which can be used to HAPI FHIR includes an interceptor which can be used to
implement CORS support on your server. See HAPI's implement CORS support on your server. See HAPI's
<a href="./doc_cors.html">CORS Documentation</a> for information <a href="./doc_cors.html">CORS Documentation</a> for information
on how to use this interceptor. on how to use this interceptor.
</p> </p>
</subsection> </subsection>
<subsection name="Rejecting Unsupported HTTP Verbs"> <subsection name="Rejecting Unsupported HTTP Verbs">
<p> <p>
Some security audit tools require that servers return an HTTP 405 if Some security audit tools require that servers return an HTTP 405 if
an unsupported HTTP verb is received (e.g. TRACE). The an unsupported HTTP verb is received (e.g. TRACE). The
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptor.html">BanUnsupprtedHttpMethodsInterceptor</a> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptor.html">BanUnsupprtedHttpMethodsInterceptor</a>
(<a href="./xref/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptor.html">code</a>) (<a href="./xref/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptor.html">code</a>)
can be used to accomplish this. can be used to accomplish this.
</p> </p>
</subsection> </subsection>
</section> <subsection name="Capturing Meta.source from an HTTP Header">
<section name="Creating Interceptors"> <p>
If you wish to override the value of <code>Resource.meta.source</code> using the value
<p> supplied in an HTTP header, you can use the
Creating your own interceptors is easy. HAPI-FHIR provides a class called <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/CaptureResourceSourceFromHeaderInterceptor.html">CaptureResourceSourceFromHeaderInterceptor</a>
<code>InterceptorAdapter</code> which you can extend and then override any (<a href="./xref/ca/uhn/fhir/rest/server/interceptor/CaptureResourceSourceFromHeaderInterceptor.html">code</a>)
methods you wish. The following example shows a simple request counter. to accomplish this.
</p> </p>
<macro name="snippet">
<param name="id" value="interceptor" /> </subsection>
<param name="file" value="examples/src/main/java/example/RequestCounterInterceptor.java" />
</macro> </section>
<p> <section name="Creating Interceptors">
The following example shows an exception handling interceptor which
overrides the built-in exception handling by providing a custom response. <p>
</p> Creating your own interceptors is easy. HAPI-FHIR provides a class called
<macro name="snippet"> <code>InterceptorAdapter</code> which you can extend and then override any
<param name="id" value="interceptor" /> methods you wish. The following example shows a simple request counter.
<param name="file" value="examples/src/main/java/example/RequestExceptionInterceptor.java" /> </p>
</macro> <macro name="snippet">
<param name="id" value="interceptor" />
</section> <param name="file" value="examples/src/main/java/example/RequestCounterInterceptor.java" />
</macro>
<section name="JPA Server Interceptors">
<p>
<p> The following example shows an exception handling interceptor which
The HAPI <a href="./doc_jpa.html">JPA Server</a> is an added layer on top of the HAPI overrides the built-in exception handling by providing a custom response.
REST server framework. When you </p>
</p> <macro name="snippet">
<param name="id" value="interceptor" />
<p> <param name="file" value="examples/src/main/java/example/RequestExceptionInterceptor.java" />
When an interceptor is registered against a RestfulServer which is backed by the </macro>
HAPI JPA Server,
the <code>incomingRequestPreHandled</code> method will be called once for most </section>
operations (e.g. a FHIR <code>create</code>), but in the case where the client
performs a FHIR <code>transaction</code> that method might be called multiple <section name="JPA Server Interceptors">
times over the course of a single client invocation. For example if the transaction
contained a single create, the <code>incomingRequestPreHandled</code> method will <p>
be called twice: once to indicate the transaction, and once to indicate the create. The HAPI <a href="./doc_jpa.html">JPA Server</a> is an added layer on top of the HAPI
</p> REST server framework. When you
</p>
<p>
This behaviour can be useful in cases where you want to audit exactly what was done <p>
over the course of a request. Since a transaction can contain creates, updates, and When an interceptor is registered against a RestfulServer which is backed by the
even more nested transactions, this behaviour ensures that you are notified for each HAPI JPA Server,
activity. the <code>incomingRequestPreHandled</code> method will be called once for most
</p> operations (e.g. a FHIR <code>create</code>), but in the case where the client
performs a FHIR <code>transaction</code> that method might be called multiple
<p> times over the course of a single client invocation. For example if the transaction
You may also choose to create interceptors which implement the contained a single create, the <code>incomingRequestPreHandled</code> method will
more specialized be called twice: once to indicate the transaction, and once to indicate the create.
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.html">IJpaServerInterceptor</a> </p>
interface, as this interceptor adds additional methods which are called during the JPA
lifecycle. <p>
</p> This behaviour can be useful in cases where you want to audit exactly what was done
over the course of a request. Since a transaction can contain creates, updates, and
<p> even more nested transactions, this behaviour ensures that you are notified for each
Note that this documentation used to <b>erroneously</b> suggest that in order to achieve activity.
this behaviour you needed to register the interceptor with the DaoConfig. In actual fact this </p>
did not make any difference, and registering interceptors with the DaoConfig has now been
deprecated since it does not achieve anything extra. <p>
</p> You may also choose to create interceptors which implement the
more specialized
</section> <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerOperationInterceptor.html">IJpaServerInterceptor</a>
interface, as this interceptor adds additional methods which are called during the JPA
</body> lifecycle.
</p>
</document>
<p>
Note that this documentation used to <b>erroneously</b> suggest that in order to achieve
this behaviour you needed to register the interceptor with the DaoConfig. In actual fact this
did not make any difference, and registering interceptors with the DaoConfig has now been
deprecated since it does not achieve anything extra.
</p>
</section>
</body>
</document>