Add new interceptor method which enabled interceptors to modify response

This commit is contained in:
James Agnew 2018-01-28 14:01:20 -06:00
parent 0677f35847
commit f976b7bf7e
24 changed files with 1291 additions and 16929 deletions

View File

@ -0,0 +1,121 @@
package example;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@SuppressWarnings("unused")
public class Copier {
private static final Logger ourLog = LoggerFactory.getLogger(Copier.class);
public static void main(String[] args) {
FhirContext ctx = FhirContext.forDstu3();
IGenericClient source = ctx.newRestfulGenericClient("http://localhost:8080/baseDstu3");
IGenericClient target = ctx.newRestfulGenericClient("https://try.smilecdr.com:8000");
List<String> resType = Arrays.asList(
"Patient", "Organization", "Encounter", "Procedure",
"Observation", "ResearchSubject", "Specimen",
"ResearchStudy", "Location", "Practitioner"
);
List<IBaseResource> queued = new ArrayList<>();
Set<String> sent = new HashSet<>();
for (String next : resType) {
copy(ctx, source, target, next, queued, sent);
}
while (queued.size() > 0) {
ourLog.info("Have {} queued resources to deliver", queued.size());
for (IBaseResource nextQueued : new ArrayList<>(queued)) {
String missingRef = null;
for (ResourceReferenceInfo nextRefInfo : ctx.newTerser().getAllResourceReferences(nextQueued)) {
String nextRef = nextRefInfo.getResourceReference().getReferenceElement().getValue();
if (isNotBlank(nextRef) && !sent.contains(nextRef)) {
missingRef = nextRef;
}
}
if (missingRef != null) {
ourLog.info("Can't send {} because of missing ref {}", nextQueued.getIdElement().getIdPart(), missingRef);
continue;
}
IIdType newId = target
.update()
.resource(nextQueued)
.execute()
.getId();
ourLog.info("Copied resource {} and got ID {}", nextQueued.getIdElement().getValue(), newId);
sent.add(nextQueued.getIdElement().toUnqualifiedVersionless().getValue());
queued.remove(nextQueued);
}
}
}
private static void copy(FhirContext theCtx, IGenericClient theSource, IGenericClient theTarget, String theResType, List<IBaseResource> theQueued, Set<String> theSent) {
Bundle received = theSource
.search()
.forResource(theResType)
.returnBundle(Bundle.class)
.execute();
copy(theCtx, theTarget, theResType, theQueued, theSent, received);
while (received.getLink("next") != null) {
ourLog.info("Fetching next page...");
received = theSource.loadPage().next(received).execute();
copy(theCtx, theTarget, theResType, theQueued, theSent, received);
}
}
private static void copy(FhirContext theCtx, IGenericClient theTarget, String theResType, List<IBaseResource> theQueued, Set<String> theSent, Bundle theReceived) {
for (Bundle.BundleEntryComponent nextEntry : theReceived.getEntry()) {
Resource nextResource = nextEntry.getResource();
nextResource.setId(theResType + "/" + "CR-" + nextResource.getIdElement().getIdPart());
boolean haveUnsentReference = false;
for (ResourceReferenceInfo nextRefInfo : theCtx.newTerser().getAllResourceReferences(nextResource)) {
IIdType nextRef = nextRefInfo.getResourceReference().getReferenceElement();
if (nextRef.hasIdPart()) {
String newRef = nextRef.getResourceType() + "/" + "CR-" + nextRef.getIdPart();
ourLog.info("Changing reference {} to {}", nextRef.getValue(), newRef);
nextRefInfo.getResourceReference().setReference(newRef);
if (!theSent.contains(newRef)) {
haveUnsentReference = true;
}
}
}
if (haveUnsentReference) {
ourLog.info("Queueing {} for delivery after", nextResource.getId());
theQueued.add(nextResource);
continue;
}
IIdType newId = theTarget
.update()
.resource(nextResource)
.execute()
.getId();
ourLog.info("Copied resource {} and got ID {}", nextResource.getId(), newId);
theSent.add(nextResource.getIdElement().toUnqualifiedVersionless().getValue());
}
}
}

View File

@ -82,6 +82,21 @@ public enum FhirVersionEnum {
return ordinal() >= theVersion.ordinal(); return ordinal() >= theVersion.ordinal();
} }
/**
* Returns the {@link FhirVersionEnum} which corresponds to a specific version of
* FHIR. Partial version strings (e.g. "3.0") are acceptable.
*
* @return Returns null if no version exists matching the given string
*/
public static FhirVersionEnum forVersionString(String theVersionString) {
for (FhirVersionEnum next : values()) {
if (next.getFhirVersionString().startsWith(theVersionString)) {
return next;
}
}
return null;
}
public boolean isEquivalentTo(FhirVersionEnum theVersion) { public boolean isEquivalentTo(FhirVersionEnum theVersion) {
if (this.equals(theVersion)) { if (this.equals(theVersion)) {
return true; return true;

View File

@ -1,4 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
@ -11,23 +12,26 @@
<artifactId>hapi-fhir-converter</artifactId> <artifactId>hapi-fhir-converter</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>HAPI FHIR - Converter</name>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId> <artifactId>hapi-fhir-base</artifactId>
<version>3.3.0-SNAPSHOT</version> <version>3.3.0-SNAPSHOT</version>
</dependency> </dependency>
<!-- Server -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId> <artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
@ -64,5 +68,60 @@
<version>3.3.0-SNAPSHOT</version> <version>3.3.0-SNAPSHOT</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- Testing -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<name>HAPI FHIR - Converter</name>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project> </project>

View File

@ -0,0 +1,72 @@
package ca.uhn.hapi.converters.server;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
import org.hl7.fhir.convertors.VersionConvertor_30_40;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.StringTokenizer;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class VersionedApiConverterInterceptor extends InterceptorAdapter {
private VersionConvertor_30_40 myVersionConvertor_30_40 = new VersionConvertor_30_40();
@Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException {
String accept = defaultString(theServletRequest.getHeader(Constants.HEADER_ACCEPT));
StringTokenizer tok = new StringTokenizer(accept, ";");
String wantVersionString = null;
while (tok.hasMoreTokens()) {
String next = tok.nextToken().trim();
if (next.startsWith("fhir-version=")) {
wantVersionString = next.substring("fhir-version=".length()).trim();
break;
}
}
FhirVersionEnum wantVersion = null;
if (isNotBlank(wantVersionString)) {
wantVersion = FhirVersionEnum.forVersionString(wantVersionString);
}
FhirVersionEnum haveVersion = theResponseObject.getStructureFhirVersionEnum();
IBaseResource converted = null;
try {
if (wantVersion == FhirVersionEnum.R4 && haveVersion == FhirVersionEnum.DSTU3) {
converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.dstu3.model.Resource) theResponseObject);
} else if (wantVersion == FhirVersionEnum.DSTU3 && haveVersion == FhirVersionEnum.R4) {
converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.r4.model.Resource) theResponseObject);
} else if (wantVersion == FhirVersionEnum.DSTU3 && haveVersion == FhirVersionEnum.R4) {
converted = myVersionConvertor_30_40.convertResource((org.hl7.fhir.r4.model.Resource) theResponseObject);
}
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
if (converted != null) {
Set<SummaryEnum> objects = Collections.emptySet();
try {
RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), converted, objects, 200, "OK", false, false, theRequestDetails, null, null);
return false;
} catch (IOException e) {
throw new InternalErrorException(e);
}
}
return true;
}
}

View File

@ -0,0 +1,276 @@
package ca.uhn.hapi.converters.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.SearchStyleEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.ClientProtocolException;
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.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.HumanName;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
public class SearchDstu3Test {
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static TokenAndListParam ourIdentifiers;
private static String ourLastMethod;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchDstu3Test.class);
private static int ourPort;
private static Server ourServer;
@Before
public void before() {
ourLastMethod = null;
ourIdentifiers = null;
}
@Test
public void testSearchNormal() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar");
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("search", ourLastMethod);
assertEquals("foo", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem());
assertEquals("bar", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
public void testSearchWithInvalidChain() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier.chain=foo%7Cbar");
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(400, status.getStatusLine().getStatusCode());
OperationOutcome oo = (OperationOutcome) ourCtx.newJsonParser().parseResource(responseContent);
assertEquals(
"Invalid search parameter \"identifier.chain\". Parameter contains a chain (.chain) and chains are not supported for this parameter (chaining is only allowed on reference parameters)",
oo.getIssueFirstRep().getDiagnostics());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@Test
public void testPagingPreservesEncodingJson() throws Exception {
HttpGet httpGet;
String linkNext;
Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=json");
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json"));
}
@Test
public void testPagingPreservesEncodingApplicationJsonFhir() throws Exception {
HttpGet httpGet;
String linkNext;
Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=" + Constants.CT_FHIR_JSON_NEW);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_JSON_NEW)));
}
private Bundle executeAndReturnLinkNext(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException {
CloseableHttpResponse status = ourClient.execute(httpGet);
Bundle bundle;
try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertEquals(theExpectEncoding, ct);
bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size());
String linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertNotNull(linkNext);
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
return bundle;
}
@Test
public void testSearchWithPostAndInvalidParameters() {
IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
LoggingInterceptor interceptor = new LoggingInterceptor();
interceptor.setLogRequestSummary(true);
interceptor.setLogRequestBody(true);
interceptor.setLogRequestHeaders(false);
interceptor.setLogResponseBody(false);
interceptor.setLogResponseHeaders(false);
interceptor.setLogResponseSummary(false);
client.registerInterceptor(interceptor);
try {
client
.search()
.forResource(Patient.class)
.where(new StringClientParam("foo").matches().value("bar"))
.prettyPrint()
.usingStyle(SearchStyleEnum.POST)
.returnBundle(Bundle.class)
.encodedJson()
.execute();
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Invalid request: The FHIR endpoint on this server does not know how to handle POST operation[Patient/_search] with parameters [[_pretty, foo]]"));
}
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@SuppressWarnings("rawtypes")
@Search()
public List search(
@RequiredParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) {
ourLastMethod = "search";
ourIdentifiers = theIdentifiers;
ArrayList<Patient> retVal = new ArrayList<Patient>();
for (int i = 0; i < 200; i++) {
Patient patient = new Patient();
patient.addName(new HumanName().setFamily("FAMILY"));
patient.getIdElement().setValue("Patient/" + i);
retVal.add(patient);
}
return retVal;
}
}
}

View File

@ -19,6 +19,7 @@ import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.mockito.internal.util.collections.ListUtil; import org.mockito.internal.util.collections.ListUtil;
import org.thymeleaf.util.ListUtils; import org.thymeleaf.util.ListUtils;
@ -53,6 +54,9 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
} }
} }
@Test @Test
public void testOverrideAndDisableBuiltInSearchParametersWithOverridingEnabled() { public void testOverrideAndDisableBuiltInSearchParametersWithOverridingEnabled() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); myDaoConfig.setDefaultSearchParamsCanBeOverridden(true);
@ -883,6 +887,53 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
} }
@Test
@Ignore
public void testSearchForStringOnIdentifierWithSpecificSystem() {
SearchParameter fooSp = new SearchParameter();
fooSp.setBase(ResourceTypeEnum.PATIENT);
fooSp.setCode("foo");
fooSp.setType(SearchParamTypeEnum.STRING);
fooSp.setXpath("Patient.identifier.where(system = 'http://AAA').value");
fooSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless();
mySearchParamRegsitry.forceRefresh();
Patient pat = new Patient();
pat.addIdentifier().setSystem("http://AAA").setValue("BAR678");
pat.setGender(AdministrativeGenderEnum.MALE);
IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
Patient pat2 = new Patient();
pat2.addIdentifier().setSystem("http://BBB").setValue("BAR678");
pat2.setGender(AdministrativeGenderEnum.FEMALE);
myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Partial match
map = new SearchParameterMap();
map.add("foo", new StringParam("bar"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Non match
map = new SearchParameterMap();
map.add("foo", new StringParam("zzz"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
}
@Test @Test
public void testSearchWithCustomParam() { public void testSearchWithCustomParam() {

View File

@ -913,6 +913,51 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
} }
@Test
public void testSearchForStringOnIdentifierWithSpecificSystem() {
SearchParameter fooSp = new SearchParameter();
fooSp.addBase("Patient");
fooSp.setCode("foo");
fooSp.setType(org.hl7.fhir.r4.model.Enumerations.SearchParamType.STRING);
fooSp.setTitle("FOO SP");
fooSp.setExpression("Patient.identifier.where(system = 'http://AAA').value");
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless();
mySearchParamRegsitry.forceRefresh();
Patient pat = new Patient();
pat.addIdentifier().setSystem("http://AAA").setValue("BAR678");
pat.setGender(AdministrativeGender.MALE);
IIdType patId = myPatientDao.create(pat, mySrd).getId().toUnqualifiedVersionless();
Patient pat2 = new Patient();
pat2.addIdentifier().setSystem("http://BBB").setValue("BAR678");
pat2.setGender(AdministrativeGender.FEMALE);
myPatientDao.create(pat2, mySrd).getId().toUnqualifiedVersionless();
SearchParameterMap map;
IBundleProvider results;
List<String> foundResources;
// Partial match
map = new SearchParameterMap();
map.add("foo", new StringParam("bar"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Non match
map = new SearchParameterMap();
map.add("foo", new StringParam("zzz"));
results = myPatientDao.search(map);
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, empty());
}
@Test @Test
public void testSearchWithCustomParam() { public void testSearchWithCustomParam() {

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
@ -67,12 +68,14 @@ public class ResourceProviderInterceptorDstu2Test extends BaseResourceProviderDs
when(myServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myJpaServerInterceptor.handleException(any(RequestDetails.class), any(BaseServerResponseException.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myJpaServerInterceptor.handleException(any(RequestDetails.class), any(BaseServerResponseException.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myJpaServerInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myJpaServerInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myJpaServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myJpaServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myJpaServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true); when(myJpaServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true);
when(myJpaServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myJpaServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myJpaServerInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
myDaoConfig.getInterceptors().add(myDaoInterceptor); myDaoConfig.getInterceptors().add(myDaoInterceptor);
ourRestServer.registerInterceptor(myServerInterceptor); ourRestServer.registerInterceptor(myServerInterceptor);

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
@ -89,6 +90,7 @@ public class ResourceProviderInterceptorDstu3Test extends BaseResourceProviderDs
when(myServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
} }
@Test @Test

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
@ -84,6 +85,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes
when(myServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myServerInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
} }
@Test @Test

View File

@ -38,6 +38,7 @@ import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
@ -131,6 +132,68 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
return retVal; return retVal;
} }
@Test
public void testSelfReferentialInclude() {
Location loc1 = new Location();
loc1.setName("loc1");
IIdType loc1id = myClient.create().resource(loc1).execute().getId().toUnqualifiedVersionless();
Location loc2 = new Location();
loc2.setName("loc2");
IIdType loc2id = myClient.create().resource(loc2).execute().getId().toUnqualifiedVersionless();
loc1 = new Location();
loc1.setId(loc1id);
loc1.setName("loc1");
loc1.getPartOf().setReference(loc2id.getValue());
myClient.update().resource(loc1).execute().getId().toUnqualifiedVersionless();
loc2 = new Location();
loc2.setId(loc2id);
loc2.setName("loc2");
loc2.getPartOf().setReference(loc1id.getValue());
myClient.update().resource(loc2).execute().getId().toUnqualifiedVersionless();
IBaseBundle result = myClient
.search()
.forResource(Location.class)
.where(Location.NAME.matches().value("loc1"))
.include(Location.INCLUDE_PARTOF.asRecursive())
.execute();
assertThat(toUnqualifiedVersionlessIdValues(result), contains(loc1id.getValue(), loc2id.getValue()));
}
@Test
public void testSelfReferentialRevInclude() {
Location loc1 = new Location();
loc1.setName("loc1");
IIdType loc1id = myClient.create().resource(loc1).execute().getId().toUnqualifiedVersionless();
Location loc2 = new Location();
loc2.setName("loc2");
IIdType loc2id = myClient.create().resource(loc2).execute().getId().toUnqualifiedVersionless();
loc1 = new Location();
loc1.setId(loc1id);
loc1.setName("loc1");
loc1.getPartOf().setReference(loc2id.getValue());
myClient.update().resource(loc1).execute().getId().toUnqualifiedVersionless();
loc2 = new Location();
loc2.setId(loc2id);
loc2.setName("loc2");
loc2.getPartOf().setReference(loc1id.getValue());
myClient.update().resource(loc2).execute().getId().toUnqualifiedVersionless();
IBaseBundle result = myClient
.search()
.forResource(Location.class)
.where(Location.NAME.matches().value("loc1"))
.revInclude(Location.INCLUDE_PARTOF.asRecursive())
.execute();
assertThat(toUnqualifiedVersionlessIdValues(result), contains(loc1id.getValue(), loc2id.getValue()));
}
/** /**
* See #484 * See #484
*/ */

View File

@ -0,0 +1,49 @@
package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* @see ca.uhn.fhir.rest.server.interceptor.IServerInterceptor
*/
public class ResponseDetails {
private IBaseResource myResponseResource;
private int myResponseCode;
/**
* Constructor
*/
public ResponseDetails() {
super();
}
/**
* Constructor
*/
public ResponseDetails(IBaseResource theResponseResource) {
setResponseResource(theResponseResource);
}
public int getResponseCode() {
return myResponseCode;
}
public void setResponseCode(int theResponseCode) {
myResponseCode = theResponseCode;
}
/**
* Get the resource which will be returned to the client
*/
public IBaseResource getResponseResource() {
return myResponseResource;
}
/**
* Set the resource which will be returned to the client
*/
public void setResponseResource(IBaseResource theResponseResource) {
myResponseResource = theResponseResource;
}
}

View File

@ -581,7 +581,7 @@ public class RestfulServerUtils {
return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null); return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
} }
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStausCode, String theStatusMessage, public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage,
boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated) boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
throws IOException { throws IOException {
IRestfulResponse response = theRequestDetails.getResponse(); IRestfulResponse response = theRequestDetails.getResponse();
@ -636,7 +636,7 @@ public class RestfulServerUtils {
} }
} }
return response.sendAttachmentResponse(bin, theStausCode, contentType); return response.sendAttachmentResponse(bin, theStatusCode, contentType);
} }
// Ok, we're not serving a binary resource, so apply default encoding // Ok, we're not serving a binary resource, so apply default encoding
@ -683,7 +683,7 @@ public class RestfulServerUtils {
} }
String charset = Constants.CHARSET_NAME_UTF8; String charset = Constants.CHARSET_NAME_UTF8;
Writer writer = response.getResponseWriter(theStausCode, theStatusMessage, contentType, charset, respondGzip); Writer writer = response.getResponseWriter(theStatusCode, theStatusMessage, contentType, charset, respondGzip);
if (theResource == null) { if (theResource == null) {
// No response is being returned // No response is being returned
} else if (encodingDomainResourceAsText && theResource instanceof IResource) { } else if (encodingDomainResourceAsText && theResource instanceof IResource) {
@ -693,7 +693,7 @@ public class RestfulServerUtils {
parser.encodeResourceToWriter(theResource, writer); parser.encodeResourceToWriter(theResource, writer);
} }
//FIXME resource leak //FIXME resource leak
return response.sendWriterResponse(theStausCode, contentType, charset, writer); return response.sendWriterResponse(theStatusCode, contentType, charset, writer);
} }
public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) {

View File

@ -20,26 +20,29 @@ package ca.uhn.fhir.rest.server.interceptor;
* #L% * #L%
*/ */
import java.io.IOException;
import java.util.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
@ -65,27 +68,21 @@ public interface IServerInterceptor {
* should return <code>false</code>, to indicate that they have handled the request and processing should stop. * should return <code>false</code>, to indicate that they have handled the request and processing should stop.
* </p> * </p>
* *
* * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the
* @param theRequestDetails
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link javax.servlet.http.HttpServletRequest servlet request}. Note that the bean * pulled out of the {@link javax.servlet.http.HttpServletRequest servlet request}. Note that the bean
* properties are not all guaranteed to be populated, depending on how early during processing the * properties are not all guaranteed to be populated, depending on how early during processing the
* exception occurred. * exception occurred.
* @param theServletRequest * @param theServletRequest The incoming request
* The incoming request * @param theServletResponse The response. Note that interceptors may choose to provide a response (i.e. by calling
* @param theServletResponse
* The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link javax.servlet.http.HttpServletResponse#getWriter()}) but in that case it is important to return * {@link javax.servlet.http.HttpServletResponse#getWriter()}) but in that case it is important to return
* <code>false</code> to indicate that the server itself should not also provide a response. * <code>false</code> to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you * If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors * must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called. * will be called.
* @throws ServletException * @throws ServletException If this exception is thrown, it will be re-thrown up to the container for handling.
* If this exception is thrown, it will be re-thrown up to the container for handling. * @throws IOException If this exception is thrown, it will be re-thrown up to the container for handling.
* @throws IOException
* If this exception is thrown, it will be re-thrown up to the container for handling.
*/ */
boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
throws ServletException, IOException; throws ServletException, IOException;
@ -93,22 +90,18 @@ public interface IServerInterceptor {
/** /**
* This method is called just before the actual implementing server method is invoked. * This method is called just before the actual implementing server method is invoked.
* *
* @param theRequestDetails * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}. * pulled out of the {@link HttpServletRequest servlet request}.
* @param theRequest * @param theRequest The incoming request
* The incoming request * @param theResponse The response. Note that interceptors may choose to provide a response (i.e. by calling
* @param theResponse
* The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code>
* to indicate that the server itself should not also provide a response. * to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you * If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors * must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called. * will be called.
* @throws AuthenticationException * @throws AuthenticationException This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
*/ */
boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException; boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException;
@ -125,10 +118,8 @@ public interface IServerInterceptor {
* will be aborted with an appropriate error returned to the client. * will be aborted with an appropriate error returned to the client.
* </p> * </p>
* *
* @param theOperation * @param theOperation The type of operation that the FHIR server has determined that the client is trying to invoke
* The type of operation that the FHIR server has determined that the client is trying to invoke * @param theProcessedRequest An object which will be populated with the details which were extracted from the raw request by the
* @param theProcessedRequest
* An object which will be populated with the details which were extracted from the raw request by the
* server, e.g. the FHIR operation type and the parsed resource body (if any). * server, e.g. the FHIR operation type and the parsed resource body (if any).
*/ */
void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theProcessedRequest); void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theProcessedRequest);
@ -140,10 +131,8 @@ public interface IServerInterceptor {
* Note that any exceptions thrown by this method will not be trapped by HAPI (they will be passed up to the server) * Note that any exceptions thrown by this method will not be trapped by HAPI (they will be passed up to the server)
* </p> * </p>
* *
* @param theRequest * @param theRequest The incoming request
* The incoming request * @param theResponse The response. Note that interceptors may choose to provide a response (i.e. by calling
* @param theResponse
* The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code>
* to indicate that the server itself should not also provide a response. * to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
@ -181,30 +170,52 @@ public interface IServerInterceptor {
* This method is called after the server implementation method has been called, but before any attempt to stream the * This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client. * response back to the client.
* *
* @param theRequestDetails * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the
* A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}. * pulled out of the {@link HttpServletRequest servlet request}.
* @param theResponseObject * @param theResponseObject The actual object which is being streamed to the client as a response. This may be
* The actual object which is being streamed to the client as a response. This may be
* <code>null</code> if the response does not include a resource. * <code>null</code> if the response does not include a resource.
* @param theServletRequest * @param theServletRequest The incoming request
* The incoming request * @param theServletResponse The response. Note that interceptors may choose to provide a response (i.e. by calling
* @param theServletResponse
* The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code>
* to indicate that the server itself should not also provide a response. * to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you * If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors * must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called. * will be called.
* @throws AuthenticationException * @throws AuthenticationException This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
* @deprecated As of HAPI FHIR 3.3.0, this method has been deprecated in
* favour of {@link #outgoingResponse(RequestDetails, ResponseDetails, HttpServletRequest, HttpServletResponse)}
* and will be removed in a future version of HAPI FHIR.
*/ */
boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
throws AuthenticationException; throws AuthenticationException;
/**
* This method is called after the server implementation method has been called, but before any attempt to stream the
* response back to the client.
*
* @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
* pulled out of the {@link HttpServletRequest servlet request}.
* @param theResponseDetails This object contains details about the response, including
* the actual payload that will be returned
* @param theServletRequest The incoming request
* @param theServletResponse The response. Note that interceptors may choose to provide a response (i.e. by calling
* {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code>
* to indicate that the server itself should not also provide a response.
* @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do.
* If your interceptor is providing a response rather than letting HAPI handle the response normally, you
* must return <code>false</code>. In this case, no further processing will occur and no further interceptors
* will be called.
* @throws AuthenticationException This exception may be thrown to indicate that the interceptor has detected an unauthorized access
* attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client.
*/
boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
throws AuthenticationException;
/** /**
* Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead * Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead
* *
@ -253,8 +264,7 @@ public interface IServerInterceptor {
* which the interceptors were registered with the server. * which the interceptors were registered with the server.
* </p> * </p>
* *
* @param theRequestDetails * @param theRequestDetails The request itself
* The request itself
*/ */
void processingCompletedNormally(ServletRequestDetails theRequestDetails); void processingCompletedNormally(ServletRequestDetails theRequestDetails);
@ -301,10 +311,8 @@ public interface IServerInterceptor {
/** /**
* Constructor * Constructor
* *
* @param theRequestDetails * @param theRequestDetails The request details to wrap
* The request details to wrap * @param theId The ID of the resource being created (note that the ID should have the resource type populated)
* @param theId
* The ID of the resource being created (note that the ID should have the resource type populated)
*/ */
public ActionRequestDetails(RequestDetails theRequestDetails, IIdType theId) { public ActionRequestDetails(RequestDetails theRequestDetails, IIdType theId) {
this(theRequestDetails, theId.getResourceType(), theId); this(theRequestDetails, theId.getResourceType(), theId);

View File

@ -26,6 +26,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.api.TagList;
@ -85,6 +86,11 @@ public class InterceptorAdapter implements IServerInterceptor {
return true; return true;
} }
@Override
public boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException {
return true;
}
@Override @Override
public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject) { public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject) {
ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; ServletRequestDetails details = (ServletRequestDetails) theRequestDetails;

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding; import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
@ -301,7 +302,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
} }
@Override @Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) public boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
throws AuthenticationException { throws AuthenticationException {
/* /*
@ -365,7 +366,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse);
} }
streamResponse(theRequestDetails, theServletResponse, theResponseObject, theServletRequest, 200); streamResponse(theRequestDetails, theServletResponse, theResponseObject.getResponseResource(), theServletRequest, 200);
return false; return false;
} }

View File

@ -20,38 +20,30 @@ package ca.uhn.fhir.rest.server.method;
* #L% * #L%
*/ */
import java.io.IOException; import ca.uhn.fhir.context.ConfigurationException;
import java.io.Writer; import ca.uhn.fhir.context.FhirContext;
import java.lang.reflect.Method; import ca.uhn.fhir.rest.api.*;
import java.util.Collections; import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import java.util.EnumSet; import ca.uhn.fhir.rest.api.server.IRestfulServer;
import java.util.Set; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import javax.servlet.http.HttpServletResponse; import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.ConfigurationException; import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.context.FhirContext; import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.parser.IParser; import java.io.IOException;
import ca.uhn.fhir.rest.api.Constants; import java.lang.reflect.Method;
import ca.uhn.fhir.rest.api.EncodingEnum; import java.util.Collections;
import ca.uhn.fhir.rest.api.MethodOutcome; import java.util.EnumSet;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import java.util.Set;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
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.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> { abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class); static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class);
@ -181,6 +173,7 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
return returnResponse(theServer, theRequest, response, outcome, resource); return returnResponse(theServer, theRequest, response, outcome, resource);
} }
public boolean isReturnVoid() { public boolean isReturnVoid() {
return myReturnVoid; return myReturnVoid;
} }
@ -206,12 +199,28 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
} }
} }
ResponseDetails responseDetails = new ResponseDetails();
responseDetails.setResponseResource(outcome);
responseDetails.setResponseCode(operationStatus);
HttpServletRequest servletRequest = null;
HttpServletResponse servletResponse = null;
if (theRequest instanceof ServletRequestDetails) {
servletRequest = ((ServletRequestDetails) theRequest).getServletRequest();
servletResponse = ((ServletRequestDetails) theRequest).getServletResponse();
}
for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = theServer.getInterceptors().get(i); IServerInterceptor next = theServer.getInterceptors().get(i);
boolean continueProcessing = next.outgoingResponse(theRequest, outcome); boolean continueProcessing = next.outgoingResponse(theRequest, outcome);
if (!continueProcessing) { if (!continueProcessing) {
return null; return null;
} }
continueProcessing = next.outgoingResponse(theRequest, responseDetails, servletRequest, servletResponse);
if (!continueProcessing) {
return null;
}
} }
IRestfulResponse restfulResponse = theRequest.getResponse(); IRestfulResponse restfulResponse = theRequest.getResponse();
@ -236,35 +245,9 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
Set<SummaryEnum> summaryMode = Collections.emptySet(); Set<SummaryEnum> summaryMode = Collections.emptySet();
return restfulResponse.streamResponseAsResource(outcome, prettyPrint, summaryMode, operationStatus, null, theRequest.isRespondGzip(), true); return restfulResponse.streamResponseAsResource(responseDetails.getResponseResource(), prettyPrint, summaryMode, responseDetails.getResponseCode(), null, theRequest.isRespondGzip(), true);
// return theRequest.getResponse().returnResponse(ParseAction.create(outcome), operationStatus, allowPrefer, response, getResourceName());
} }
protected void streamOperationOutcome(BaseServerResponseException theE, RestfulServer theServer, EncodingEnum theEncodingNotNull, HttpServletResponse theResponse, RequestDetails theRequest) throws IOException {
theResponse.setStatus(theE.getStatusCode());
theServer.addHeadersToResponse(theResponse);
if (theE.getOperationOutcome() != null) {
theResponse.setContentType(theEncodingNotNull.getResourceContentType());
IParser parser = theEncodingNotNull.newParser(theServer.getFhirContext());
parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(theServer, theRequest));
Writer writer = theResponse.getWriter();
try {
parser.encodeResourceToWriter(theE.getOperationOutcome(), writer);
} finally {
writer.close();
}
} else {
theResponse.setContentType(Constants.CT_TEXT);
Writer writer = theResponse.getWriter();
try {
writer.append(theE.getMessage());
} finally {
writer.close();
}
}
}
protected static void parseContentLocation(FhirContext theContext, MethodOutcome theOutcomeToPopulate, String theLocationHeader) { protected static void parseContentLocation(FhirContext theContext, MethodOutcome theOutcomeToPopulate, String theLocationHeader) {
if (StringUtils.isBlank(theLocationHeader)) { if (StringUtils.isBlank(theLocationHeader)) {

View File

@ -1,6 +1,41 @@
package ca.uhn.fhir.rest.server.method; package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
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;
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.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/* /*
* #%L * #%L
* HAPI FHIR - Server Framework * HAPI FHIR - Server Framework
@ -20,33 +55,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import ca.uhn.fhir.util.CollectionUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.UrlUtil;
public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> { public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> {
protected static final Set<String> ALLOWED_PARAMS; protected static final Set<String> ALLOWED_PARAMS;
@ -115,40 +123,111 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
} }
public MethodReturnTypeEnum getMethodReturnType() { protected IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
return myMethodReturnType; IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
int numToReturn;
String searchId = null;
List<IBaseResource> resourceList;
Integer numTotalResults = theResult.size();
if (theServer.getPagingProvider() == null) {
numToReturn = numTotalResults;
if (numToReturn > 0) {
resourceList = theResult.getResources(0, numToReturn);
} else {
resourceList = Collections.emptyList();
}
RestfulServerUtils.validateResourceListNotNull(resourceList);
} else {
IPagingProvider pagingProvider = theServer.getPagingProvider();
if (theLimit == null || theLimit.equals(Integer.valueOf(0))) {
numToReturn = pagingProvider.getDefaultPageSize();
} else {
numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit);
} }
@Override if (numTotalResults != null) {
public String getResourceName() { numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
return myResourceName;
} }
/** if (numToReturn > 0) {
* If the response is a bundle, this type will be placed in the root of the bundle (can be null) resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
} else {
resourceList = Collections.emptyList();
}
RestfulServerUtils.validateResourceListNotNull(resourceList);
if (theSearchId != null) {
searchId = theSearchId;
} else {
if (numTotalResults == null || numTotalResults > numToReturn) {
searchId = pagingProvider.storeResultList(theResult);
if (isBlank(searchId)) {
ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults);
}
}
}
}
/*
* Remove any null entries in the list - This generally shouldn't happen but can if
* data has been manually purged from the JPA database
*/ */
protected abstract BundleTypeEnum getResponseBundleType(); boolean hasNull = false;
for (IBaseResource next : resourceList) {
public abstract ReturnTypeEnum getReturnType(); if (next == null) {
hasNull = true;
@Override break;
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { }
}
IBaseResource response = doInvokeServer(theServer, theRequest); if (hasNull) {
for (Iterator<IBaseResource> iter = resourceList.iterator(); iter.hasNext(); ) {
Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); if (iter.next() == null) {
iter.remove();
for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { }
IServerInterceptor next = theServer.getInterceptors().get(i);
boolean continueProcessing = next.outgoingResponse(theRequest, response);
if (!continueProcessing) {
return null;
} }
} }
/*
* Make sure all returned resources have an ID (if not, this is a bug
* in the user server code)
*/
for (IBaseResource next : resourceList) {
if (next.getIdElement() == null || next.getIdElement().isEmpty()) {
if (!(next instanceof BaseOperationOutcome)) {
throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)");
}
}
}
String serverBase = theRequest.getFhirServerBase();
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
return theRequest.getResponse().streamResponseAsResource(response, prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(), isAddContentLocationHeader()); String linkPrev = null;
String linkNext = null;
if (searchId != null) {
if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType));
}
if (theOffset > 0) {
int start = Math.max(0, theOffset - theLimit);
linkPrev = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, start, theLimit, theRequest.getParameters(), prettyPrint, theBundleType);
}
}
bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), serverBase, theLinkSelf, linkPrev, linkNext, theResult.size(), theBundleType, theResult.getPublished());
bundleFactory.addResourcesToBundle(new ArrayList<>(resourceList), theBundleType, serverBase, theServer.getBundleInclusionRule(), theIncludes);
if (theServer.getPagingProvider() != null) {
int limit;
limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize();
limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize());
}
return bundleFactory.getResourceBundle();
} }
@ -261,111 +340,60 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
return responseObject; return responseObject;
} }
protected IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes, public MethodReturnTypeEnum getMethodReturnType() {
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) { return myMethodReturnType;
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
int numToReturn;
String searchId = null;
List<IBaseResource> resourceList;
Integer numTotalResults = theResult.size();
if (theServer.getPagingProvider() == null) {
numToReturn = numTotalResults;
if (numToReturn > 0) {
resourceList = theResult.getResources(0, numToReturn);
} else {
resourceList = Collections.emptyList();
}
RestfulServerUtils.validateResourceListNotNull(resourceList);
} else {
IPagingProvider pagingProvider = theServer.getPagingProvider();
if (theLimit == null || theLimit.equals(Integer.valueOf(0))) {
numToReturn = pagingProvider.getDefaultPageSize();
} else {
numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit);
} }
if (numTotalResults != null) { @Override
numToReturn = Math.min(numToReturn, numTotalResults - theOffset); public String getResourceName() {
return myResourceName;
} }
if (numToReturn > 0) { protected void setResourceName(String theResourceName) {
resourceList = theResult.getResources(theOffset, numToReturn + theOffset); myResourceName = theResourceName;
} else {
resourceList = Collections.emptyList();
}
RestfulServerUtils.validateResourceListNotNull(resourceList);
if (theSearchId != null) {
searchId = theSearchId;
} else {
if (numTotalResults == null || numTotalResults > numToReturn) {
searchId = pagingProvider.storeResultList(theResult);
if (isBlank(searchId)) {
ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults);
}
}
}
} }
/* /**
* Remove any null entries in the list - This generally shouldn't happen but can if * If the response is a bundle, this type will be placed in the root of the bundle (can be null)
* data has been manually purged from the JPA database
*/ */
boolean hasNull = false; protected abstract BundleTypeEnum getResponseBundleType();
for (IBaseResource next : resourceList) {
if (next == null) { public abstract ReturnTypeEnum getReturnType();
hasNull = true;
break; @Override
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
IBaseResource response = doInvokeServer(theServer, theRequest);
Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest);
ResponseDetails responseDetails = new ResponseDetails();
responseDetails.setResponseResource(response);
responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK);
HttpServletRequest servletRequest = null;
HttpServletResponse servletResponse = null;
if (theRequest instanceof ServletRequestDetails) {
servletRequest = ((ServletRequestDetails) theRequest).getServletRequest();
servletResponse = ((ServletRequestDetails) theRequest).getServletResponse();
} }
for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = theServer.getInterceptors().get(i);
boolean continueProcessing = next.outgoingResponse(theRequest, response);
if (!continueProcessing) {
return null;
} }
if (hasNull) {
for (Iterator<IBaseResource> iter = resourceList.iterator(); iter.hasNext(); ) { continueProcessing = next.outgoingResponse(theRequest, responseDetails, servletRequest, servletResponse);
if (iter.next() == null) { if (!continueProcessing) {
iter.remove(); return null;
}
} }
} }
/*
* Make sure all returned resources have an ID (if not, this is a bug
* in the user server code)
*/
for (IBaseResource next : resourceList) {
if (next.getIdElement() == null || next.getIdElement().isEmpty()) {
if (!(next instanceof BaseOperationOutcome)) {
throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)");
}
}
}
String serverBase = theRequest.getFhirServerBase();
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
String linkPrev = null; return theRequest.getResponse().streamResponseAsResource(responseDetails.getResponseResource(), prettyPrint, summaryMode, responseDetails.getResponseCode(), null, theRequest.isRespondGzip(), isAddContentLocationHeader());
String linkNext = null;
if (searchId != null) {
if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType));
}
if (theOffset > 0) {
int start = Math.max(0, theOffset - theLimit);
linkPrev = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, start, theLimit, theRequest.getParameters(), prettyPrint, theBundleType);
}
}
bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), serverBase, theLinkSelf, linkPrev, linkNext, theResult.size(), theBundleType, theResult.getPublished());
bundleFactory.addResourcesToBundle(new ArrayList<>(resourceList), theBundleType, serverBase, theServer.getBundleInclusionRule(), theIncludes);
if (theServer.getPagingProvider() != null) {
int limit;
limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize();
limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize());
}
return bundleFactory.getResourceBundle();
} }
@ -378,10 +406,6 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
return true; return true;
} }
protected void setResourceName(String theResourceName) {
myResourceName = theResourceName;
}
public enum MethodReturnTypeEnum { public enum MethodReturnTypeEnum {
BUNDLE, BUNDLE,
BUNDLE_PROVIDER, BUNDLE_PROVIDER,

View File

@ -15,6 +15,7 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
@ -66,9 +67,11 @@ public class InterceptorDstu2_1Test {
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor2.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
//@formatter:off //@formatter:off
String input = String input =
@ -107,7 +110,9 @@ public class InterceptorDstu2_1Test {
order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture()); order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture());
order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class)); order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
// Avoid concurrency issues // Avoid concurrency issues
Thread.sleep(500); Thread.sleep(500);

View File

@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
@ -153,7 +154,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
// true means it decided to not handle the request.. // true means it decided to not handle the request..
assertTrue(ic.outgoingResponse(reqDetails, resource, req, resp)); assertTrue(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
} }
@ -408,7 +409,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
// false means it decided to handle the request.. // false means it decided to handle the request..
assertFalse(ic.outgoingResponse(reqDetails, resource, req, resp)); assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
} }
/** /**
@ -442,7 +443,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
// false means it decided to handle the request.. // false means it decided to handle the request..
assertFalse(ic.outgoingResponse(reqDetails, resource, req, resp)); assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
} }
@Test @Test
@ -475,7 +476,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
// true means it decided to not handle the request.. // true means it decided to not handle the request..
assertTrue(ic.outgoingResponse(reqDetails, resource, req, resp)); assertTrue(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
} }
@ -504,7 +505,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServer(new RestfulServer(ourCtx)); reqDetails.setServer(new RestfulServer(ourCtx));
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
assertFalse(ic.outgoingResponse(reqDetails, resource, req, resp)); assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
String output = sw.getBuffer().toString(); String output = sw.getBuffer().toString();
ourLog.info(output); ourLog.info(output);
@ -540,7 +541,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServer(new RestfulServer(ourCtx)); reqDetails.setServer(new RestfulServer(ourCtx));
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
assertFalse(ic.outgoingResponse(reqDetails, resource, req, resp)); assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
String output = sw.getBuffer().toString(); String output = sw.getBuffer().toString();
ourLog.info(output); ourLog.info(output);
@ -579,7 +580,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServer(server); reqDetails.setServer(server);
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
assertFalse(ic.outgoingResponse(reqDetails, resource, req, resp)); assertFalse(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
String output = sw.getBuffer().toString(); String output = sw.getBuffer().toString();
ourLog.info(output); ourLog.info(output);
@ -615,7 +616,7 @@ public class ResponseHighlightingInterceptorTest {
reqDetails.setServletRequest(req); reqDetails.setServletRequest(req);
// True here means the interceptor didn't handle the request, because HTML wasn't the top ranked accept header // True here means the interceptor didn't handle the request, because HTML wasn't the top ranked accept header
assertTrue(ic.outgoingResponse(reqDetails, resource, req, resp)); assertTrue(ic.outgoingResponse(reqDetails, new ResponseDetails(resource), req, resp));
} }
/** /**

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
@ -213,6 +214,7 @@ public class ServerActionInterceptorTest {
when(ourInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(ourInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(ourInterceptor.outgoingResponse(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(ourInterceptor.outgoingResponse(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(ourInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(ourInterceptor.outgoingResponse(any(RequestDetails.class), any(IBaseResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(ourInterceptor.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
} }
public static class PlainProvider { public static class PlainProvider {

View File

@ -2,13 +2,12 @@ package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
@ -19,6 +18,8 @@ import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
@ -28,7 +29,7 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.hamcrest.core.StringContains; import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
@ -38,9 +39,11 @@ import org.mockito.InOrder;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -81,6 +84,35 @@ public class InterceptorDstu3Test {
"}"; "}";
} }
@Test
public void testModifyResponse() throws IOException {
InterceptorAdapter interceptor = new InterceptorAdapter() {
@Override
public boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException {
Patient retVal = new Patient();
retVal.setId(theResponseDetails.getResponseResource().getIdElement());
retVal.addName().setFamily("NAME1");
theResponseDetails.setResponseResource(retVal);
theResponseDetails.setResponseCode(202);
return true;
}
};
ourServlet.registerInterceptor(interceptor);
try {
HttpGet get = new HttpGet("http://localhost:" + ourPort + "/Patient/1");
try (CloseableHttpResponse status = ourClient.execute(get)) {
String response = IOUtils.toString(status.getEntity().getContent(), Constants.CHARSET_UTF8);
assertThat(response, containsString("NAME1"));
assertEquals(202, status.getStatusLine().getStatusCode());
assertEquals("Accepted", status.getStatusLine().getReasonPhrase());
}
} finally {
ourServlet.unregisterInterceptor(interceptor);
}
}
@Test @Test
public void testResourceResponseIncluded() throws Exception { public void testResourceResponseIncluded() throws Exception {
ourServlet.setInterceptors(myInterceptor1, myInterceptor2); ourServlet.setInterceptors(myInterceptor1, myInterceptor2);
@ -88,9 +120,11 @@ public class InterceptorDstu3Test {
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor2.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
String input = createInput(); String input = createInput();
@ -109,7 +143,9 @@ public class InterceptorDstu3Test {
order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture()); order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(opTypeCapt.capture(), arTypeCapt.capture());
order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class)); order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IBaseResource.class)); order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IBaseResource.class));
order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IBaseResource.class)); order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IBaseResource.class));
order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
// Avoid concurrency issues // Avoid concurrency issues
Thread.sleep(500); Thread.sleep(500);
@ -123,21 +159,6 @@ public class InterceptorDstu3Test {
assertNotNull(arTypeCapt.getValue().getResource()); assertNotNull(arTypeCapt.getValue().getResource());
} }
public void testModifyResponse() {
InterceptorAdapter interceptor = new InterceptorAdapter(){
@Override
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
ServletRequestDetails srd = (ServletRequestDetails)theRequestDetails;
String input = new String(srd.loadRequestContents(), Constants.CHARSET_UTF8);
assertThat(input, StringContains.containsString("\"active\":true"));
String newInput = createInput().replace("true", "false");
srd.setRequestContents(newInput.getBytes(Constants.CHARSET_UTF8));
return true;
}
};
}
@Test @Test
public void testResponseWithNothing() throws Exception { public void testResponseWithNothing() throws Exception {
ourServlet.setInterceptors(myInterceptor1); ourServlet.setInterceptors(myInterceptor1);
@ -145,6 +166,7 @@ public class InterceptorDstu3Test {
when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true);
when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(ResponseDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
String input = createInput(); String input = createInput();
@ -240,7 +262,6 @@ public class InterceptorDstu3Test {
public static class DummyPatientResourceProvider implements IResourceProvider { public static class DummyPatientResourceProvider implements IResourceProvider {
@Create() @Create()
public MethodOutcome create(@ResourceParam Patient theResource) { public MethodOutcome create(@ResourceParam Patient theResource) {
ourLastPatient = theResource; ourLastPatient = theResource;
@ -252,6 +273,14 @@ public class InterceptorDstu3Test {
return Patient.class; return Patient.class;
} }
@Read
public Patient read(@IdParam IdType theId) {
Patient retVal = new Patient();
retVal.setId(theId);
retVal.addName().setFamily("NAME0");
return retVal;
}
@Validate() @Validate()
public MethodOutcome validate(@ResourceParam Patient theResource) { public MethodOutcome validate(@ResourceParam Patient theResource) {
return new MethodOutcome(); return new MethodOutcome();

View File

@ -8,15 +8,15 @@
<body> <body>
<release version="3.3.0" date="TBD"> <release version="3.3.0" date="TBD">
<action type="add"> <action type="add">
This release corrects an ineffiency in the JPA Server, but requires a schema This release corrects an inefficiency in the JPA Server, but requires a schema
change in order to update. Prior to this version of HAPI FHIR, a CLOB column change in order to update. Prior to this version of HAPI FHIR, a CLOB column
containing the complete resource body was stored in two containing the complete resource body was stored in two
tables: HFJ_RESOURCE and HFJ_RES_VER. Because the same content was stored in two tables: HFJ_RESOURCE and HFJ_RES_VER. Because the same content was stored in two
places, the database consumed more space than is needed to. places, the database consumed more space than is needed to.
<![CDATA[<br/><br/>]]> <![CDATA[<br/><br/>]]>
In order to reduce this duplication, the columns have been removed from the In order to reduce this duplication, two columns have been removed from the
HFJ_RESOURCE column. This means that on any database that is being upgraded HFJ_RESOURCE table. This means that on any database that is being upgraded
to HAPI FHIR 3.2.0, you will need to remove the columns to HAPI FHIR 3.3.0+, you will need to remove the columns
<![CDATA[<code>RES_TEXT</code> and <code>RES_ENCODING</code>]]> (or <![CDATA[<code>RES_TEXT</code> and <code>RES_ENCODING</code>]]> (or
set them to nullable if you want an easy means of rolling back). Naturally set them to nullable if you want an easy means of rolling back). Naturally
you should back your database up prior to making this change. you should back your database up prior to making this change.
@ -24,7 +24,7 @@
<action type="fix"> <action type="fix">
Fix a crash in the JSON parser when parsing extensions on repeatable Fix a crash in the JSON parser when parsing extensions on repeatable
elements (e.g. Patient.address.line) where there is an extension on the elements (e.g. Patient.address.line) where there is an extension on the
first repetion but not on subsequent repetitions of the first repetition but not on subsequent repetitions of the
repeatable primitive. Thanks to Igor Sirkovich for providing a repeatable primitive. Thanks to Igor Sirkovich for providing a
test case! test case!
</action> </action>
@ -32,6 +32,19 @@
Fix an issue where the JPA server crashed while attempting to normalize string values Fix an issue where the JPA server crashed while attempting to normalize string values
containing Korean text. Thanks to GitHub user @JoonggeonLee for reporting! containing Korean text. Thanks to GitHub user @JoonggeonLee for reporting!
</action> </action>
<action type="add">
A new method overload has been added to IServerInterceptor:
<![CDATA[
<code>outgoingResponse(RequestDetails, ResponseDetails, HttpServletRequest, HttpServletResponse)
]]>. This new method allows an interceptor to completely replace
the resource being returned with a different resource instance, or
to modify the HTTP Status Code being returned. All other "outgoingResponse"
methods have been deprecated and are recommended to be migrated
to the new method. This new method (with its RequestDetails and ResponseDetails
parameters) should be flexible enough to
accommodate future needs which means that this should be the last
time we have to change it.
</action>
</release> </release>
<release version="3.2.0" date="2018-01-13"> <release version="3.2.0" date="2018-01-13">
<action type="add"> <action type="add">