Fix #750 - Elements are not preserved in page requests

This commit is contained in:
James Agnew 2017-11-06 19:49:50 -05:00
parent 2b4a492870
commit 59f4177a59
9 changed files with 1148 additions and 999 deletions

View File

@ -43,7 +43,7 @@ public abstract class BaseParser implements IParser {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class);
private ContainedResources myContainedResources; private ContainedResources myContainedResources;
private boolean myEncodeElementsAppliesToChildResourcesOnly;
private FhirContext myContext; private FhirContext myContext;
private Set<String> myDontEncodeElements; private Set<String> myDontEncodeElements;
private boolean myDontEncodeElementsIncludesStars; private boolean myDontEncodeElementsIncludesStars;
@ -556,6 +556,16 @@ public abstract class BaseParser implements IParser {
&& theIncludedResource == false; && theIncludedResource == false;
} }
@Override
public boolean isEncodeElementsAppliesToChildResourcesOnly() {
return myEncodeElementsAppliesToChildResourcesOnly;
}
@Override
public void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly) {
myEncodeElementsAppliesToChildResourcesOnly = theEncodeElementsAppliesToChildResourcesOnly;
}
@Override @Override
public boolean isOmitResourceId() { public boolean isOmitResourceId() {
return myOmitResourceId; return myOmitResourceId;
@ -1039,7 +1049,13 @@ public abstract class BaseParser implements IParser {
} }
private boolean checkIfParentShouldBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) { private boolean checkIfParentShouldBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) {
return checkIfPathMatchesForEncoding(thePathBuilder, theStarPass, myEncodeElementsAppliesToResourceTypes, myEncodeElements, true); Set<String> encodeElements = myEncodeElements;
if (encodeElements != null && encodeElements.isEmpty() == false) {
if (isEncodeElementsAppliesToChildResourcesOnly() && !mySubResource) {
encodeElements = null;
}
}
return checkIfPathMatchesForEncoding(thePathBuilder, theStarPass, myEncodeElementsAppliesToResourceTypes, encodeElements, true);
} }
private boolean checkIfParentShouldNotBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) { private boolean checkIfParentShouldNotBeEncodedAndBuildPath(StringBuilder thePathBuilder, boolean theStarPass) {
@ -1058,6 +1074,9 @@ public abstract class BaseParser implements IParser {
} else { } else {
thePathBuilder.append(myResDef.getName()); thePathBuilder.append(myResDef.getName());
} }
if (theElements == null) {
return true;
}
if (theElements.contains(thePathBuilder.toString())) { if (theElements.contains(thePathBuilder.toString())) {
return true; return true;
} }

View File

@ -206,6 +206,22 @@ public interface IParser {
*/ */
void setEncodeElements(Set<String> theEncodeElements); void setEncodeElements(Set<String> theEncodeElements);
/**
* If set to <code>true</code> (default is false), the values supplied
* to {@link #setEncodeElements(Set)} will not be applied to the root
* resource (typically a Bundle), but will be applied to any sub-resources
* contained within it (i.e. search result resources in that bundle)
*/
void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly);
/**
* If set to <code>true</code> (default is false), the values supplied
* to {@link #setEncodeElements(Set)} will not be applied to the root
* resource (typically a Bundle), but will be applied to any sub-resources
* contained within it (i.e. search result resources in that bundle)
*/
boolean isEncodeElementsAppliesToChildResourcesOnly();
/** /**
* If provided, tells the parse which resource types to apply {@link #setEncodeElements(Set) encode elements} to. Any * If provided, tells the parse which resource types to apply {@link #setEncodeElements(Set) encode elements} to. Any
* resource types not specified here will be encoded completely, with no elements excluded. * resource types not specified here will be encoded completely, with no elements excluded.

View File

@ -19,11 +19,12 @@ public class BinaryUtil {
public static IBaseReference getSecurityContext(FhirContext theCtx, IBaseBinary theBinary) { public static IBaseReference getSecurityContext(FhirContext theCtx, IBaseBinary theBinary) {
RuntimeResourceDefinition def = theCtx.getResourceDefinition("Binary"); RuntimeResourceDefinition def = theCtx.getResourceDefinition("Binary");
BaseRuntimeChildDefinition child = def.getChildByName("securityContext"); BaseRuntimeChildDefinition child = def.getChildByName("securityContext");
List<IBase> values = child.getAccessor().getValues(theBinary);
IBaseReference retVal = null; IBaseReference retVal = null;
if (values.size() > 0) { if (child != null) {
retVal = (IBaseReference) values.get(0); List<IBase> values = child.getAccessor().getValues(theBinary);
if (values.size() > 0) {
retVal = (IBaseReference) values.get(0);
}
} }
return retVal; return retVal;
} }

View File

@ -86,10 +86,41 @@ public class RestfulServerUtils {
} }
} }
if (elements != null && elements.size() > 0) { if (elements != null && elements.size() > 0) {
Set<String> newElements = new HashSet<String>(); Set<String> newElements = new HashSet<>();
for (String next : elements) { for (String next : elements) {
newElements.add("*." + next); newElements.add("*." + next);
} }
/*
* We try to be smart about what the user is asking for
* when they include an _elements parameter. If we're responding
* to something that returns a Bundle (e.g. a search) we assume
* the elements don't apply to the Bundle itself, unless
* the client has explicitly scoped the Bundle
* (i.e. with Bundle.total or something like that)
*/
switch (theRequestDetails.getRestOperationType()) {
case SEARCH_SYSTEM:
case SEARCH_TYPE:
case HISTORY_SYSTEM:
case HISTORY_TYPE:
case HISTORY_INSTANCE:
case GET_PAGE:
boolean haveExplicitBundleElement = false;
for (String next : newElements) {
if (next.startsWith("Bundle.")) {
haveExplicitBundleElement = true;
break;
}
}
if (!haveExplicitBundleElement) {
parser.setEncodeElementsAppliesToChildResourcesOnly(true);
}
break;
default:
break;
}
parser.setEncodeElements(newElements); parser.setEncodeElements(newElements);
parser.setEncodeElementsAppliesToResourceTypes(elementsAppliesTo); parser.setEncodeElementsAppliesToResourceTypes(elementsAppliesTo);
} }
@ -147,6 +178,19 @@ public class RestfulServerUtils {
b.append(theBundleType.getCode()); b.append(theBundleType.getCode());
} }
String paramName = Constants.PARAM_ELEMENTS;
String[] params = theRequestParameters.get(paramName);
if (params != null) {
for (String nextValue : params) {
if (isNotBlank(nextValue)) {
b.append('&');
b.append(paramName);
b.append('=');
b.append(UrlUtil.escape(nextValue));
}
}
}
return b.toString(); return b.toString();
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new Error("UTF-8 not supported", e);// should not happen throw new Error("UTF-8 not supported", e);// should not happen
@ -587,9 +631,11 @@ public class RestfulServerUtils {
response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;");
IBaseReference securityContext = BinaryUtil.getSecurityContext(theServer.getFhirContext(), bin); IBaseReference securityContext = BinaryUtil.getSecurityContext(theServer.getFhirContext(), bin);
String securityContextRef = securityContext.getReferenceElement().getValue(); if (securityContext != null) {
if (isNotBlank(securityContextRef)) { String securityContextRef = securityContext.getReferenceElement().getValue();
response.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, securityContextRef); if (isNotBlank(securityContextRef)) {
response.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, securityContextRef);
}
} }
return response.sendAttachmentResponse(bin, theStausCode, contentType); return response.sendAttachmentResponse(bin, theStausCode, contentType);

View File

@ -332,7 +332,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
} }
bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), serverBase, theLinkSelf, linkPrev, linkNext, theResult.size(), theBundleType, theResult.getPublished()); bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), serverBase, theLinkSelf, linkPrev, linkNext, theResult.size(), theBundleType, theResult.getPublished());
bundleFactory.addResourcesToBundle(new ArrayList<IBaseResource>(resourceList), theBundleType, serverBase, theServer.getBundleInclusionRule(), theIncludes); bundleFactory.addResourcesToBundle(new ArrayList<>(resourceList), theBundleType, serverBase, theServer.getBundleInclusionRule(), theIncludes);
if (theServer.getPagingProvider() != null) { if (theServer.getPagingProvider() != null) {
int limit; int limit;

View File

@ -1499,7 +1499,7 @@ public class JsonParserDstu2_1Test {
String val = ourCtx.newJsonParser().encodeResourceToString(patient); String val = ourCtx.newJsonParser().encodeResourceToString(patient);
String expected = "{\"resourceType\":\"Binary\",\"id\":\"11\",\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}"; String expected = "{\"resourceType\":\"Binary\",\"id\":\"11\",\"meta\":{\"versionId\":\"22\"},\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}";
ourLog.info("Expected: {}", expected); ourLog.info("Expected: {}", expected);
ourLog.info("Actual : {}", val); ourLog.info("Actual : {}", val);
assertEquals(expected, val); assertEquals(expected, val);

View File

@ -1,32 +1,5 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
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.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
import org.junit.*;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Search;
@ -38,15 +11,46 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.*; 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.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Patient;
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 SearchR4Test { public class SearchR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchR4Test.class);
private static CloseableHttpClient ourClient; private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4(); private static FhirContext ourCtx = FhirContext.forR4();
private static TokenAndListParam ourIdentifiers; private static TokenAndListParam ourIdentifiers;
private static String ourLastMethod; private static String ourLastMethod;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchR4Test.class);
private static int ourPort; private static int ourPort;
private static Server ourServer; private static Server ourServer;
@ -57,74 +61,63 @@ public class SearchR4Test {
ourIdentifiers = null; ourIdentifiers = null;
} }
@Test private Bundle executeAndReturnLinkNext(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException, ClientProtocolException {
public void testSearchNormal() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar");
CloseableHttpResponse status = ourClient.execute(httpGet); CloseableHttpResponse status = ourClient.execute(httpGet);
Bundle bundle;
try { try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent); ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertEquals("search", ourLastMethod); assertEquals(theExpectEncoding, ct);
bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals("foo", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem()); assertEquals(10, bundle.getEntry().size());
assertEquals("bar", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue()); String linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertNotNull(linkNext);
} finally { } finally {
IOUtils.closeQuietly(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent());
} }
return bundle;
} }
@Test @Test
public void testSearchWithInvalidChain() throws Exception { public void testPagingPreservesElements() 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; HttpGet httpGet;
String linkNext; String linkNext;
Bundle bundle; Bundle bundle;
String linkSelf;
// Initial search // Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=json"); httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_elements=name");
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
assertThat(toJson(bundle), not(containsString("active")));
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertThat(linkSelf, containsString("_elements=name"));
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json")); assertThat(linkNext, containsString("_elements=name"));
ourLog.info(toJson(bundle));
// Fetch the next page // Fetch the next page
httpGet = new HttpGet(linkNext); httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
assertThat(toJson(bundle), not(containsString("active")));
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json")); assertThat(linkNext, containsString("_elements=name"));
// Fetch the next page // Fetch the next page
httpGet = new HttpGet(linkNext); httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
assertThat(toJson(bundle), not(containsString("active")));
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json")); assertThat(linkNext, containsString("_elements=name"));
// Fetch the next page // Fetch the next page
httpGet = new HttpGet(linkNext); httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
assertThat(toJson(bundle), not(containsString("active")));
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=json")); assertThat(linkNext, containsString("_elements=name"));
} }
@ -161,34 +154,35 @@ public class SearchR4Test {
} }
@Test @Test
public void testPagingPreservesEncodingXml() throws Exception { public void testPagingPreservesEncodingJson() throws Exception {
HttpGet httpGet; HttpGet httpGet;
String linkNext; String linkNext;
Bundle bundle; Bundle bundle;
// Initial search // Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=xml"); httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=json");
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
assertThat(toJson(bundle), containsString("active"));
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml")); assertThat(linkNext, containsString("_format=json"));
// Fetch the next page // Fetch the next page
httpGet = new HttpGet(linkNext); httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml")); assertThat(linkNext, containsString("_format=json"));
// Fetch the next page // Fetch the next page
httpGet = new HttpGet(linkNext); httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml")); assertThat(linkNext, containsString("_format=json"));
// Fetch the next page // Fetch the next page
httpGet = new HttpGet(linkNext); httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML); bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.JSON);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml")); assertThat(linkNext, containsString("_format=json"));
} }
@ -260,26 +254,76 @@ public class SearchR4Test {
} }
private Bundle executeAndReturnLinkNext(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException, ClientProtocolException { @Test
CloseableHttpResponse status = ourClient.execute(httpGet); public void testPagingPreservesEncodingXml() throws Exception {
HttpGet httpGet;
String linkNext;
Bundle bundle; Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar&_format=xml");
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml"));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnLinkNext(httpGet, EncodingEnum.XML);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertThat(linkNext, containsString("_format=xml"));
}
@Test
public void testSearchNormal() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?identifier=foo%7Cbar");
CloseableHttpResponse status = ourClient.execute(httpGet);
try { try {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent); ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertEquals(theExpectEncoding, ct); assertEquals("search", ourLastMethod);
bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size()); assertEquals("foo", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem());
String linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl(); assertEquals("bar", ourIdentifiers.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue());
assertNotNull(linkNext);
} finally { } finally {
IOUtils.closeQuietly(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent());
} }
return bundle;
}
@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 @Test
public void testSearchWithPostAndInvalidParameters() throws Exception { public void testSearchWithPostAndInvalidParameters() throws Exception {
IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort); IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
@ -293,14 +337,14 @@ public class SearchR4Test {
client.registerInterceptor(interceptor); client.registerInterceptor(interceptor);
try { try {
client client
.search() .search()
.forResource(Patient.class) .forResource(Patient.class)
.where(new StringClientParam("foo").matches().value("bar")) .where(new StringClientParam("foo").matches().value("bar"))
.prettyPrint() .prettyPrint()
.usingStyle(SearchStyleEnum.POST) .usingStyle(SearchStyleEnum.POST)
.returnBundle(org.hl7.fhir.r4.model.Bundle.class) .returnBundle(org.hl7.fhir.r4.model.Bundle.class)
.encodedJson() .encodedJson()
.execute(); .execute();
fail(); fail();
} catch (InvalidRequestException e) { } 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]]")); 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]]"));
@ -308,6 +352,10 @@ public class SearchR4Test {
} }
private String toJson(Bundle theBundle) {
return ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(theBundle);
}
@AfterClass @AfterClass
public static void afterClassClearContext() throws Exception { public static void afterClassClearContext() throws Exception {
ourServer.stop(); ourServer.stop();
@ -349,16 +397,17 @@ public class SearchR4Test {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@Search() @Search()
public List search( public List search(
@RequiredParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) { @RequiredParam(name = Patient.SP_IDENTIFIER) TokenAndListParam theIdentifiers) {
ourLastMethod = "search"; ourLastMethod = "search";
ourIdentifiers = theIdentifiers; ourIdentifiers = theIdentifiers;
ArrayList<Patient> retVal = new ArrayList<Patient>(); ArrayList<Patient> retVal = new ArrayList<Patient>();
for (int i = 0; i < 200; i++) { for (int i = 0; i < 200; i++) {
Patient patient = new Patient(); Patient patient = new Patient();
patient.addName(new HumanName().setFamily("FAMILY")); patient.addName(new HumanName().setFamily("FAMILY"));
patient.setActive(true);
patient.getIdElement().setValue("Patient/" + i); patient.getIdElement().setValue("Patient/" + i);
retVal.add((Patient) patient); retVal.add(patient);
} }
return retVal; return retVal;
} }

View File

@ -175,11 +175,26 @@
was not encoded correctly. Thanks to Malcolm McRoberts for the pull was not encoded correctly. Thanks to Malcolm McRoberts for the pull
request with fix and test case! request with fix and test case!
</action> </action>
<action type="add">
Bundle resources did not have their version encoded when serializing
in FHIR resource (XML/JSON) format.
</action>
<action type="add"> <action type="add">
The Binary resource endpoint now supports the `X-Security-Context` header when The Binary resource endpoint now supports the `X-Security-Context` header when
reading or writing Binary contents using their native Content-Type (i.e exchanging reading or writing Binary contents using their native Content-Type (i.e exchanging
the raw binary with the server, as opposed to exchanging a FHIR resource). the raw binary with the server, as opposed to exchanging a FHIR resource).
</action> </action>
<action type="fix">
When paging through multiple pages of search results, if the
client had requested a subset of resources to be returned using the
<![CDATA[<code>_elements</code>]]> parameter, the elements list
was lost after the first page of results.
In addition, elements will not remove elements from
search/history Bundles (i.e. elements from the Bundle itself, as opposed
to elements in the entry resources) unless the Bundle elements are
explicitly listed, e.g. <![CDATA[<code>_include=Bundle.total</code>]]>.
Thanks to @parisni for reporting!
</action>
</release> </release>
<release version="3.0.0" date="2017-09-27"> <release version="3.0.0" date="2017-09-27">
<action type="add"> <action type="add">