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,12 +19,13 @@ 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 (child != null) {
List<IBase> values = child.getAccessor().getValues(theBinary);
if (values.size() > 0) { if (values.size() > 0) {
retVal = (IBaseReference) values.get(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,10 +631,12 @@ 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);
if (securityContext != null) {
String securityContextRef = securityContext.getReferenceElement().getValue(); String securityContextRef = securityContext.getReferenceElement().getValue();
if (isNotBlank(securityContextRef)) { if (isNotBlank(securityContextRef)) {
response.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, 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,41 +1,26 @@
package ca.uhn.fhir.parser; package ca.uhn.fhir.parser;
import static org.hamcrest.Matchers.contains; import ca.uhn.fhir.context.FhirContext;
import static org.hamcrest.Matchers.containsString; import ca.uhn.fhir.model.api.annotation.Child;
import static org.hamcrest.Matchers.empty; import ca.uhn.fhir.model.api.annotation.ResourceDef;
import static org.hamcrest.Matchers.not; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import static org.hamcrest.Matchers.startsWith; import ca.uhn.fhir.parser.FooMessageHeaderWithExplicitField.FooMessageSourceComponent;
import static org.hamcrest.Matchers.stringContainsInOrder; import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation;
import static org.junit.Assert.assertArrayEquals; import ca.uhn.fhir.parser.PatientWithCustomCompositeExtension.FooParentExtension;
import static org.junit.Assert.assertEquals; import ca.uhn.fhir.util.TestUtil;
import static org.junit.Assert.assertFalse; import com.google.common.collect.Sets;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.collection.IsEmptyCollection;
import org.hamcrest.core.StringContains; import org.hamcrest.core.StringContains;
import org.hamcrest.text.StringContainsInOrder; import org.hamcrest.text.StringContainsInOrder;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Address.AddressUse; import org.hl7.fhir.dstu3.model.Address.AddressUse;
import org.hl7.fhir.dstu3.model.Address.AddressUseEnumFactory; import org.hl7.fhir.dstu3.model.Address.AddressUseEnumFactory;
import org.hl7.fhir.dstu3.model.Bundle.*; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem; import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem;
import org.hl7.fhir.dstu3.model.DiagnosticReport.DiagnosticReportStatus; import org.hl7.fhir.dstu3.model.DiagnosticReport.DiagnosticReportStatus;
import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionBindingComponent; import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionBindingComponent;
@ -52,27 +37,27 @@ import org.junit.*;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.xmlunit.builder.DiffBuilder; import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.builder.Input; import org.xmlunit.builder.Input;
import org.xmlunit.diff.*; import org.xmlunit.diff.ComparisonControllers;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.Diff;
import org.xmlunit.diff.ElementSelectors;
import com.google.common.collect.Sets; import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import ca.uhn.fhir.context.FhirContext; import static org.hamcrest.Matchers.contains;
import ca.uhn.fhir.model.api.annotation.Child; import static org.hamcrest.Matchers.*;
import ca.uhn.fhir.model.api.annotation.ResourceDef; import static org.hamcrest.Matchers.startsWith;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import static org.junit.Assert.*;
import ca.uhn.fhir.parser.FooMessageHeaderWithExplicitField.FooMessageSourceComponent; import static org.mockito.Matchers.any;
import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; import static org.mockito.Mockito.*;
import ca.uhn.fhir.parser.PatientWithCustomCompositeExtension.FooParentExtension;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.validation.IValidationContext;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationContext;
import ca.uhn.fhir.validation.ValidationResult;
public class XmlParserDstu3Test { public class XmlParserDstu3Test {
private static FhirContext ourCtx = FhirContext.forDstu3();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlParserDstu3Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlParserDstu3Test.class);
private static FhirContext ourCtx = FhirContext.forDstu3();
@After @After
public void after() { public void after() {
@ -83,21 +68,24 @@ public class XmlParserDstu3Test {
} }
@Test @Test
public void testEncodeBinaryWithSecurityContext() { public void testBaseUrlFooResourceCorrectlySerializedInExtensionValueReference() {
Binary bin = new Binary(); String refVal = "http://my.org/FooBar";
bin.setContentType("text/plain");
bin.setContent("Now is the time".getBytes());
Reference securityContext = new Reference();
securityContext.setReference("DiagnosticReport/1");
bin.setSecurityContext(securityContext);
String encoded = ourCtx.newXmlParser().encodeResourceToString(bin);
ourLog.info(encoded);
assertThat(encoded, containsString("Binary"));
assertThat(encoded, containsString("<contentType value=\"text/plain\"/>"));
assertThat(encoded, containsString("<securityContext><reference value=\"DiagnosticReport/1\"/></securityContext>"));
assertThat(encoded, containsString("<content value=\"Tm93IGlzIHRoZSB0aW1l\"/>"));
}
Patient fhirPat = new Patient();
fhirPat.addExtension().setUrl("x1").setValue(new Reference(refVal));
IParser parser = ourCtx.newXmlParser();
String output = parser.encodeResourceToString(fhirPat);
System.out.println("output: " + output);
// Deserialize then check that valueReference value is still correct
fhirPat = parser.parseResource(Patient.class, output);
List<Extension> extlst = fhirPat.getExtensionsByUrl("x1");
Assert.assertEquals(1, extlst.size());
Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference());
}
/** /**
* See #544 * See #544
@ -977,6 +965,22 @@ public class XmlParserDstu3Test {
assertEquals("<Binary xmlns=\"http://hl7.org/fhir\"><content value=\"AQIDBA==\"/></Binary>", output); assertEquals("<Binary xmlns=\"http://hl7.org/fhir\"><content value=\"AQIDBA==\"/></Binary>", output);
} }
@Test
public void testEncodeBinaryWithSecurityContext() {
Binary bin = new Binary();
bin.setContentType("text/plain");
bin.setContent("Now is the time".getBytes());
Reference securityContext = new Reference();
securityContext.setReference("DiagnosticReport/1");
bin.setSecurityContext(securityContext);
String encoded = ourCtx.newXmlParser().encodeResourceToString(bin);
ourLog.info(encoded);
assertThat(encoded, containsString("Binary"));
assertThat(encoded, containsString("<contentType value=\"text/plain\"/>"));
assertThat(encoded, containsString("<securityContext><reference value=\"DiagnosticReport/1\"/></securityContext>"));
assertThat(encoded, containsString("<content value=\"Tm93IGlzIHRoZSB0aW1l\"/>"));
}
@Test @Test
public void testEncodeBundleWithContained() { public void testEncodeBundleWithContained() {
DiagnosticReport rpt = new DiagnosticReport(); DiagnosticReport rpt = new DiagnosticReport();
@ -1872,7 +1876,7 @@ public class XmlParserDstu3Test {
{ {
IParser p = ourCtx.newXmlParser(); IParser p = ourCtx.newXmlParser();
p.setEncodeElements(new HashSet<String>(Arrays.asList("Patient.name", "Bundle.entry"))); p.setEncodeElements(new HashSet<>(Arrays.asList("Patient.name", "Bundle.entry")));
p.setPrettyPrint(true); p.setPrettyPrint(true);
String out = p.encodeResourceToString(bundle); String out = p.encodeResourceToString(bundle);
ourLog.info(out); ourLog.info(out);
@ -1908,6 +1912,31 @@ public class XmlParserDstu3Test {
} }
@Test
public void testEncodeWithEncodeElementsAppliesToChildResourcesOnly() throws Exception {
Patient patient = new Patient();
patient.getMeta().addProfile("http://profile");
patient.addName().setFamily("FAMILY");
patient.addAddress().addLine("LINE1");
Bundle bundle = new Bundle();
bundle.setTotal(100);
bundle.addEntry().setResource(patient);
{
IParser p = ourCtx.newXmlParser();
p.setEncodeElements(new HashSet<>(Arrays.asList("Patient.name")));
p.setEncodeElementsAppliesToChildResourcesOnly(true);
p.setPrettyPrint(true);
String out = p.encodeResourceToString(bundle);
ourLog.info(out);
assertThat(out, containsString("total"));
assertThat(out, containsString("Patient"));
assertThat(out, containsString("name"));
assertThat(out, not(containsString("address")));
}
}
@Test @Test
public void testEncodeWithNarrative() { public void testEncodeWithNarrative() {
Patient p = new Patient(); Patient p = new Patient();
@ -2835,7 +2864,7 @@ public class XmlParserDstu3Test {
/** /**
* See #426 * See #426
* * <p>
* Value type of FOO isn't a valid datatype * Value type of FOO isn't a valid datatype
*/ */
@Test @Test
@ -2934,6 +2963,30 @@ public class XmlParserDstu3Test {
assertEquals("value", capt.getValue()); assertEquals("value", capt.getValue());
} }
@Test
public void testParseMetaUpdatedDate() {
String input = "<Bundle xmlns=\"http://hl7.org/fhir\">\n" +
" <id value=\"e2ee823b-ee4d-472d-b79d-495c23f16b99\"/>\n" +
" <meta>\n" +
" <lastUpdated value=\"2015-06-22T15:48:57.554-04:00\"/>\n" +
" </meta>\n" +
" <type value=\"searchset\"/>\n" +
" <base value=\"http://localhost:58109/fhir/context\"/>\n" +
" <total value=\"0\"/>\n" +
" <link>\n" +
" <relation value=\"self\"/>\n" +
" <url value=\"http://localhost:58109/fhir/context/Patient?_pretty=true\"/>\n" +
" </link>\n" +
"</Bundle>";
Bundle b = ourCtx.newXmlParser().parseResource(Bundle.class, input);
InstantType updated = b.getMeta().getLastUpdatedElement();
assertEquals("2015-06-22T15:48:57.554-04:00", updated.getValueAsString());
}
@Test @Test
public void testParseMetadata() throws Exception { public void testParseMetadata() throws Exception {
@ -2987,43 +3040,6 @@ public class XmlParserDstu3Test {
} }
public static void compareXml(String content, String reEncoded) {
Diff d = DiffBuilder.compare(Input.fromString(content))
.withTest(Input.fromString(reEncoded))
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
.checkForSimilar()
.ignoreWhitespace() // this is working with newest Saxon 9.8.0-2 (not worked with 9.7.0-15
.ignoreComments() // this is not working even with newest Saxon 9.8.0-2
.withComparisonController(ComparisonControllers.Default)
.build();
assertTrue(d.toString(), !d.hasDifferences());
}
@Test
public void testParseMetaUpdatedDate() {
String input = "<Bundle xmlns=\"http://hl7.org/fhir\">\n" +
" <id value=\"e2ee823b-ee4d-472d-b79d-495c23f16b99\"/>\n" +
" <meta>\n" +
" <lastUpdated value=\"2015-06-22T15:48:57.554-04:00\"/>\n" +
" </meta>\n" +
" <type value=\"searchset\"/>\n" +
" <base value=\"http://localhost:58109/fhir/context\"/>\n" +
" <total value=\"0\"/>\n" +
" <link>\n" +
" <relation value=\"self\"/>\n" +
" <url value=\"http://localhost:58109/fhir/context/Patient?_pretty=true\"/>\n" +
" </link>\n" +
"</Bundle>";
Bundle b = ourCtx.newXmlParser().parseResource(Bundle.class, input);
InstantType updated = b.getMeta().getLastUpdatedElement();
assertEquals("2015-06-22T15:48:57.554-04:00", updated.getValueAsString());
}
// TODO: this should work // TODO: this should work
@Test @Test
@Ignore @Ignore
@ -3094,40 +3110,6 @@ public class XmlParserDstu3Test {
assertEquals("Patient", reincarnatedPatient.getIdElement().getResourceType()); assertEquals("Patient", reincarnatedPatient.getIdElement().getResourceType());
} }
/**
* See #344
*/
@Test
public void testParserIsCaseSensitive() {
Observation obs = new Observation();
SampledData data = new SampledData();
data.setData("1 2 3");
data.setOrigin((SimpleQuantity) new SimpleQuantity().setValue(0L));
data.setPeriod(1000L);
obs.setValue(data);
IParser p = ourCtx.newXmlParser().setPrettyPrint(true).setParserErrorHandler(new StrictErrorHandler());
String encoded = p.encodeResourceToString(obs);
ourLog.info(encoded);
p.parseResource(encoded);
try {
p.parseResource(encoded.replace("Observation", "observation"));
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [1,1]]: Unknown resource type 'observation': Resource names are case sensitive, found similar name: 'Observation'",
e.getMessage());
}
try {
p.parseResource(encoded.replace("valueSampledData", "valueSampleddata"));
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Unknown element 'valueSampleddata' found during parse", e.getMessage());
}
}
@Test(expected = DataFormatException.class) @Test(expected = DataFormatException.class)
public void testParseWithInvalidLocalRef() throws IOException { public void testParseWithInvalidLocalRef() throws IOException {
String string = IOUtils.toString(getClass().getResourceAsStream("/bundle_with_invalid_contained_ref.xml"), StandardCharsets.UTF_8); String string = IOUtils.toString(getClass().getResourceAsStream("/bundle_with_invalid_contained_ref.xml"), StandardCharsets.UTF_8);
@ -3226,6 +3208,40 @@ public class XmlParserDstu3Test {
} }
/**
* See #344
*/
@Test
public void testParserIsCaseSensitive() {
Observation obs = new Observation();
SampledData data = new SampledData();
data.setData("1 2 3");
data.setOrigin((SimpleQuantity) new SimpleQuantity().setValue(0L));
data.setPeriod(1000L);
obs.setValue(data);
IParser p = ourCtx.newXmlParser().setPrettyPrint(true).setParserErrorHandler(new StrictErrorHandler());
String encoded = p.encodeResourceToString(obs);
ourLog.info(encoded);
p.parseResource(encoded);
try {
p.parseResource(encoded.replace("Observation", "observation"));
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [1,1]]: Unknown resource type 'observation': Resource names are case sensitive, found similar name: 'Observation'",
e.getMessage());
}
try {
p.parseResource(encoded.replace("valueSampledData", "valueSampleddata"));
fail();
} catch (DataFormatException e) {
assertEquals("DataFormatException at [[row,col {unknown-source}]: [2,4]]: Unknown element 'valueSampleddata' found during parse", e.getMessage());
}
}
/** /**
* See #551 * See #551
*/ */
@ -3245,7 +3261,7 @@ public class XmlParserDstu3Test {
/** /**
* See #339 * See #339
* * <p>
* https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
*/ */
@Test @Test
@ -3275,35 +3291,22 @@ public class XmlParserDstu3Test {
} }
@Test
public void testBaseUrlFooResourceCorrectlySerializedInExtensionValueReference() {
String refVal = "http://my.org/FooBar";
Patient fhirPat = new Patient();
fhirPat.addExtension().setUrl("x1").setValue(new Reference(refVal));
IParser parser = ourCtx.newXmlParser();
String output = parser.encodeResourceToString(fhirPat);
System.out.println("output: " + output);
// Deserialize then check that valueReference value is still correct
fhirPat = parser.parseResource(Patient.class, output);
List<Extension> extlst = fhirPat.getExtensionsByUrl("x1");
Assert.assertEquals(1, extlst.size());
Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference());
}
@AfterClass @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();
} }
public static void main(String[] args) { public static void compareXml(String content, String reEncoded) {
IGenericClient c = ourCtx.newRestfulGenericClient("http://fhir-dev.healthintersections.com.au/open"); Diff d = DiffBuilder.compare(Input.fromString(content))
// c.registerInterceptor(new LoggingInterceptor(true)); .withTest(Input.fromString(reEncoded))
c.read().resource("Patient").withId("324").execute(); .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
.checkForSimilar()
.ignoreWhitespace() // this is working with newest Saxon 9.8.0-2 (not worked with 9.7.0-15
.ignoreComments() // this is not working even with newest Saxon 9.8.0-2
.withComparisonController(ComparisonControllers.Default)
.build();
assertTrue(d.toString(), !d.hasDifferences());
} }
@ResourceDef(name = "Patient") @ResourceDef(name = "Patient")

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,25 +254,75 @@ 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 {
@ -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();
@ -357,8 +405,9 @@ public class SearchR4Test {
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">