From d0edc03f14ffc6f2caa925144cf91bfe567f6038 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 12 Aug 2014 11:04:41 -0400 Subject: [PATCH] Linked resources in server now correctly returned --- hapi-fhir-base/src/changes/changes.xml | 7 +- ...BaseRuntimeElementCompositeDefinition.java | 12 ++ .../java/ca/uhn/fhir/context/FhirContext.java | 11 +- .../java/ca/uhn/fhir/model/api/Bundle.java | 2 - .../fhir/model/primitive/BaseDateTimeDt.java | 17 ++- .../main/java/ca/uhn/fhir/parser/IParser.java | 22 +++- .../ca/uhn/fhir/rest/param/BaseParam.java | 59 ++++++++++ .../ca/uhn/fhir/rest/param/DateParam.java | 19 +++ .../ca/uhn/fhir/rest/param/QuantityParam.java | 15 ++- .../uhn/fhir/rest/param/ReferenceParam.java | 15 +++ .../ca/uhn/fhir/rest/param/StringParam.java | 4 +- .../ca/uhn/fhir/rest/param/TokenParam.java | 2 +- .../ca/uhn/fhir/rest/server/Constants.java | 1 + .../uhn/fhir/rest/server/RestfulServer.java | 66 ++++++----- .../java/ca/uhn/fhir/util/FhirTerser.java | 2 +- hapi-fhir-base/src/site/xdoc/doc_cors.xml | 2 +- .../src/site/xdoc/doc_resource_references.xml | 33 ++++-- .../model/primitive/BaseDateTimeDtTest.java | 6 + .../ca/uhn/fhir/model/primitive/IdDtTest.java | 1 - .../IncludedResourceStitchingClientTest.java | 111 ++++++++++++++++++ .../ca/uhn/fhir/rest/server/IncludeTest.java | 84 +++++++++++++ .../org.eclipse.wst.common.component | 2 +- .../org.eclipse.wst.common.component | 2 +- 23 files changed, 434 insertions(+), 61 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java diff --git a/hapi-fhir-base/src/changes/changes.xml b/hapi-fhir-base/src/changes/changes.xml index fcbbfb29031..4861d6929e0 100644 --- a/hapi-fhir-base/src/changes/changes.xml +++ b/hapi-fhir-base/src/changes/changes.xml @@ -49,7 +49,7 @@ Transaction server method is now allowed to return an OperationOutcome in addition to the - incoming resources. The public test server now does this in ordeer to return status information + incoming resources. The public test server now does this in order to return status information about the transaction processing. @@ -67,6 +67,11 @@ Added narrative generator template for OperationOutcome resource + + Date/time types did not correctly parse values in the format "yyyymmdd" (although the FHIR-defined format + is "yyyy-mm-dd" anyhow, and this is correctly handled). Thanks to Jeffrey Ting of Systems Made Simple + for reporting! + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java index 5bc4ba0549b..b3989eab7ac 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.parser.DataFormatException; public abstract class BaseRuntimeElementCompositeDefinition extends BaseRuntimeElementDefinition { private List myChildren = new ArrayList(); + private List myChildrenAndExtensions; private Map myNameToChild = new HashMap(); public BaseRuntimeElementCompositeDefinition(String theName, Class theImplementingClass) { @@ -65,6 +66,10 @@ public abstract class BaseRuntimeElementCompositeDefinition getChildrenAndExtension() { + return myChildrenAndExtensions; + } + @Override public void sealAndInitialize(Map, BaseRuntimeElementDefinition> theClassToElementDefinitions) { super.sealAndInitialize(theClassToElementDefinitions); @@ -90,5 +95,12 @@ public abstract class BaseRuntimeElementCompositeDefinition children = new ArrayList(); + children.addAll(myChildren); + children.addAll(getExtensionsModifier()); + children.addAll(getExtensionsNonModifier()); + myChildrenAndExtensions=Collections.unmodifiableList(children); } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 44feecb337f..77b1195324e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -179,7 +179,7 @@ public class FhirContext { * Create and return a new JSON parser. * *

- * Performance Note: This class is cheap to create, and may be called once for every message being processed without incurring any performance penalty + * Performance Note: This method is cheap to call, and may be called once for every message being processed without incurring any performance penalty *

*/ public IParser newJsonParser() { @@ -192,7 +192,7 @@ public class FhirContext { * href="http://hl7api.sourceforge.net/hapi-fhir/doc_rest_client.html">RESTful Client documentation for more information on how to define this interface. * *

- * Performance Note: This class is cheap to create, and may be called once for every message being processed without incurring any performance penalty + * Performance Note: This method is cheap to call, and may be called once for every operation invocation without incurring any performance penalty *

* * @param theClientType @@ -210,8 +210,9 @@ public class FhirContext { /** * Instantiates a new generic client. A generic client is able to perform any of the FHIR RESTful operations against a compliant server, but does not have methods defining the specific * functionality required (as is the case with {@link #newRestfulClient(Class, String) non-generic clients}). + * *

- * In most cases it is preferable to use the non-generic clients instead of this mechanism, but not always. + * Performance Note: This method is cheap to call, and may be called once for every operation invocation without incurring any performance penalty *

* * @param theServerBase @@ -227,10 +228,10 @@ public class FhirContext { } /** - * Create and return a new JSON parser. + * Create and return a new XML parser. * *

- * Performance Note: This class is cheap to create, and may be called once for every message being processed without incurring any performance penalty + * Performance Note: This method is cheap to call, and may be called once for every message being processed without incurring any performance penalty *

*/ public IParser newXmlParser() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java index 02f8a7e5e58..4f242e019c3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java @@ -37,8 +37,6 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.StringDt; -import ca.uhn.fhir.rest.client.IGenericClient; -import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.Constants; public class Bundle extends BaseBundle /* implements IElement */{ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java index 52879b03b1d..8d3dae77db3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/primitive/BaseDateTimeDt.java @@ -31,6 +31,7 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; +import java.util.regex.Pattern; import javax.xml.bind.DatatypeConverter; @@ -165,13 +166,19 @@ public abstract class BaseDateTimeDt extends BasePrimitive { myValue = theValue; } + private Pattern ourYearPattern = Pattern.compile("[0-9]{4}"); + private Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}"); + private Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}"); + private Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}"); + private Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}"); + @Override public void setValueAsString(String theValue) throws DataFormatException { try { if (theValue == null) { myValue = null; clearTimeZone(); - } else if (theValue.length() == 4) { + } else if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) { if (isPrecisionAllowed(YEAR)) { setValue((ourYearFormat).parse(theValue)); setPrecision(YEAR); @@ -179,7 +186,7 @@ public abstract class BaseDateTimeDt extends BasePrimitive { } else { throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support YEAR precision): " + theValue); } - } else if (theValue.length() == 7) { + } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) { // E.g. 1984-01 (this is valid according to the spec) if (isPrecisionAllowed(MONTH)) { setValue((ourYearMonthFormat).parse(theValue)); @@ -188,16 +195,16 @@ public abstract class BaseDateTimeDt extends BasePrimitive { } else { throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support MONTH precision): " + theValue); } - } else if (theValue.length() == 8) { + } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) { //Eg. 19840101 (allow this just to be lenient) if (isPrecisionAllowed(DAY)) { setValue((ourYearMonthDayNoDashesFormat).parse(theValue)); - setPrecision(MONTH); + setPrecision(DAY); clearTimeZone(); } else { throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); } - } else if (theValue.length() == 10) { + } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) { // E.g. 1984-01-01 (this is valid according to the spec) if (isPrecisionAllowed(DAY)) { setValue((ourYearMonthDayFormat).parse(theValue)); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java index 6141959bbd4..be5e36577b7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java @@ -74,7 +74,7 @@ public interface IParser { * specify a class which extends a built-in type (e.g. a custom * type extending the default Patient class) * @param theReader - * The reader to parse inpou from + * The reader to parse input from. Note that the Reader will not be closed by the parser upon completion. * @return A parsed resource * @throws DataFormatException * If the resource can not be parsed because the data is not @@ -98,8 +98,28 @@ public interface IParser { */ T parseResource(Class theResourceType, String theString) throws DataFormatException; + /** + * Parses a resource + * + * @param theReader + * The reader to parse input from. Note that the Reader will not be closed by the parser upon completion. + * @return A parsed resource + * @throws DataFormatException + * If the resource can not be parsed because the data is not + * recognized or invalid for any reason + */ IResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException; + /** + * Parses a resource + * + * @param theString + * The string to parse + * @return A parsed resource + * @throws DataFormatException + * If the resource can not be parsed because the data is not + * recognized or invalid for any reason + */ IResource parseResource(String theMessageString) throws ConfigurationException, DataFormatException; /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java new file mode 100644 index 00000000000..55e33b8ac51 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseParam.java @@ -0,0 +1,59 @@ +package ca.uhn.fhir.rest.param; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.server.Constants; + +/** + * Base class for RESTful operation parameter types + */ +public class BaseParam implements IQueryParameterType { + + private Boolean myMissing; + + /** + * If set to non-null value, indicates that this parameter has been populated with a "[name]:missing=true" or "[name]:missing=false" vale + * instead of a normal value + */ + public Boolean getMissing() { + return myMissing; + } + + + + @Override + public String getQueryParameterQualifier() { + if (myMissing) { + return Constants.PARAMQUALIFIER_MISSING; + } + return null; + } + + + @Override + public String getValueAsQueryToken() { + if (myMissing != null) { + return myMissing ? "true" : "false"; + } + return null; + } + + + + /** + * If set to non-null value, indicates that this parameter has been populated with a "[name]:missing=true" or "[name]:missing=false" vale + * instead of a normal value + */ + public void setMissing(Boolean theMissing) { + myMissing = theMissing; + } + + @Override + public void setValueAsQueryToken(String theQualifier, String theValue) { + if (Constants.PARAMQUALIFIER_MISSING.equals(theQualifier)) { + myMissing = "true".equals(theValue); + } else { + myMissing = null; + } + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java index 90e7c44a206..a192772c2c0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java @@ -36,6 +36,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class DateParam extends DateTimeDt implements IQueryParameterType, IQueryParameterOr { private QuantityCompararatorEnum myComparator; + private BaseParam myBase=new BaseParam(); /** * Constructor @@ -78,11 +79,17 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery @Override public String getQueryParameterQualifier() { + if (myBase.getMissing()!=null) { + return myBase.getQueryParameterQualifier(); + } return null; } @Override public String getValueAsQueryToken() { + if (myBase.getMissing()!=null) { + return myBase.getValueAsQueryToken(); + } if (myComparator != null && getValue() != null) { return myComparator.getCode() + getValueAsString(); } else if (myComparator == null && getValue() != null) { @@ -112,6 +119,13 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery @Override public void setValueAsQueryToken(String theQualifier, String theValue) { + myBase.setValueAsQueryToken(theQualifier, theValue); + if (myBase.getMissing()!=null) { + setValue(null); + myComparator=null; + return; + } + if (theValue.length() < 2) { throw new DataFormatException("Invalid qualified date parameter: " + theValue); } @@ -141,8 +155,10 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery @Override public void setValuesAsQueryTokens(QualifiedParamList theParameters) { + myBase.setMissing(null); myComparator = null; setValueAsString(null); + if (theParameters.size() == 1) { setValueAsString(theParameters.get(0)); } else if (theParameters.size() > 1) { @@ -159,6 +175,9 @@ public class DateParam extends DateTimeDt implements IQueryParameterType, IQuery b.append(myComparator.getCode()); } b.append(getValueAsString()); + if (myBase.getMissing()!=null) { + b.append(" missing=").append(myBase.getMissing()); + } b.append("]"); return b.toString(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityParam.java index 10307ffc6ac..62128ecbd78 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityParam.java @@ -36,7 +36,7 @@ import ca.uhn.fhir.model.primitive.DecimalDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.UriDt; -public class QuantityParam implements IQueryParameterType { +public class QuantityParam extends BaseParam implements IQueryParameterType { private boolean myApproximate; private QuantityDt myQuantity = new QuantityDt(); @@ -131,6 +131,7 @@ public class QuantityParam implements IQueryParameterType { } private void clear() { + setMissing(null); myQuantity.setComparator((BoundCodeDt) null); myQuantity.setCode((CodeDt) null); myQuantity.setSystem((UriDt) null); @@ -162,6 +163,10 @@ public class QuantityParam implements IQueryParameterType { @Override public String getValueAsQueryToken() { + if (super.getMissing()!=null) { + return super.getValueAsQueryToken(); + } + StringBuilder b = new StringBuilder(); if (myQuantity.getComparator() != null) { b.append(ParameterUtil.escape(myQuantity.getComparator().getValue())); @@ -248,6 +253,11 @@ public class QuantityParam implements IQueryParameterType { public void setValueAsQueryToken(String theQualifier, String theValue) { clear(); + super.setValueAsQueryToken(theQualifier, theValue); + if (getMissing()!=null) { + return; + } + if (theValue == null) { return; } @@ -290,6 +300,9 @@ public class QuantityParam implements IQueryParameterType { b.append("value", myQuantity.getValue().getValueAsString()); b.append("system", myQuantity.getSystem().getValueAsString()); b.append("units", myQuantity.getUnits().getValueAsString()); + if (getMissing()!=null) { + b.append("missing", getMissing()); + } return b.toString(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java index 7a74b8c1a5e..c8d93432235 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.model.primitive.IdDt; public class ReferenceParam extends IdDt implements IQueryParameterType { private String myChain; + private BaseParam myBase=new BaseParam(); public ReferenceParam() { } @@ -58,6 +59,10 @@ public class ReferenceParam extends IdDt implements IQueryParameterType { @Override public String getQueryParameterQualifier() { + if (myBase.getMissing()!=null) { + return myBase.getQueryParameterQualifier(); + } + StringBuilder b = new StringBuilder(); if (isNotBlank(getResourceType())) { b.append(':'); @@ -75,6 +80,9 @@ public class ReferenceParam extends IdDt implements IQueryParameterType { @Override public String getValueAsQueryToken() { + if (myBase.getMissing()!=null) { + return myBase.getValueAsQueryToken(); + } return getIdPart(); } @@ -91,6 +99,13 @@ public class ReferenceParam extends IdDt implements IQueryParameterType { @Override public void setValueAsQueryToken(String theQualifier, String theValue) { + myBase.setValueAsQueryToken(theQualifier, theValue); + if (myBase.getMissing()!=null) { + myChain=null; + setValue(null); + return; + } + String q = theQualifier; String resourceType = null; if (isNotBlank(q)) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringParam.java index 797e6a4f0c2..7e548bcafb7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringParam.java @@ -30,7 +30,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.server.Constants; -public class StringParam implements IQueryParameterType { +public class StringParam extends BaseParam implements IQueryParameterType { private boolean myExact; private String myValue; @@ -52,7 +52,7 @@ public class StringParam implements IQueryParameterType { if (isExact()) { return Constants.PARAMQUALIFIER_STRING_EXACT; } else { - return null; + return super.getQueryParameterQualifier(); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java index a82846caec8..11f0caaf961 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java @@ -33,7 +33,7 @@ import ca.uhn.fhir.model.dstu.composite.IdentifierDt; import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.rest.server.Constants; -public class TokenParam implements IQueryParameterType { +public class TokenParam extends BaseParam implements IQueryParameterType { private String mySystem; private boolean myText; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index 11adbc31a0b..7a7a6f301c4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -80,6 +80,7 @@ public class Constants { public static final String PARAM_SORT_DESC = "_sort:desc"; public static final String PARAM_TAGS = "_tags"; public static final String PARAM_VALIDATE = "_validate"; + public static final String PARAMQUALIFIER_MISSING = ":missing"; public static final String PARAMQUALIFIER_STRING_EXACT = ":exact"; public static final String PARAMQUALIFIER_TOKEN_TEXT = ":text"; public static final int STATUS_HTTP_200_OK = 200; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 5e18e26483b..eb199fd7d91 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -581,13 +581,13 @@ public class RestfulServer extends HttpServlet { */ private int escapedLength(String theServletPath) { int delta = 0; - for(int i =0;i references = theContext.newTerser().getAllPopulatedChildElementsOfType(next, ResourceReferenceDt.class); - for (ResourceReferenceDt nextRef : references) { - IResource nextRes = nextRef.getResource(); - if (nextRes != null) { - if (nextRes.getId().hasIdPart()) { - IdDt id = nextRes.getId().toVersionless(); - if (id.hasResourceType()==false) { - String resName = theContext.getResourceDefinition(nextRes).getName(); - id = id.withResourceType(resName); + do { + List addedResourcesThisPass = new ArrayList(); + + for (ResourceReferenceDt nextRef : references) { + IResource nextRes = nextRef.getResource(); + if (nextRes != null) { + if (nextRes.getId().hasIdPart()) { + IdDt id = nextRes.getId().toVersionless(); + if (id.hasResourceType() == false) { + String resName = theContext.getResourceDefinition(nextRes).getName(); + id = id.withResourceType(resName); + } + + if (!addedResourceIds.contains(id)) { + addedResourceIds.add(id); + addedResourcesThisPass.add(nextRes); + } + + nextRef.setResource(null); + nextRef.setReference(id); } - - if (!addedResourceIds.contains(id)) { - addedResourceIds.add(id); - addedResources.add(nextRes); - } - - nextRef.setResource(null); - nextRef.setReference(id); } } - } - + + // Linked resources may themselves have linked resources + references = new ArrayList(); + for (IResource iResource : addedResourcesThisPass) { + List newReferences = theContext.newTerser().getAllPopulatedChildElementsOfType(iResource, ResourceReferenceDt.class); + references.addAll(newReferences); + } + + addedResources.addAll(addedResourcesThisPass); + + } while (references.isEmpty() == false); bundle.addResource(next, theContext, theServerBase); } for (IResource next : addedResources) { bundle.addResource(next, theContext, theServerBase); } - + bundle.getTotalResults().setValue(theTotalResults); return bundle; } @@ -1056,7 +1069,7 @@ public class RestfulServer extends HttpServlet { if (next.getId() == null || next.getId().isEmpty()) { if (!(next instanceof OperationOutcome)) { throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); - } + } } } @@ -1110,8 +1123,7 @@ public class RestfulServer extends HttpServlet { String fullId = theResource.getId().withServerBase(theServerBase, resName); theHttpResponse.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId); } - - + if (theResource instanceof Binary) { Binary bin = (Binary) theResource; if (isNotBlank(bin.getContentType())) { @@ -1122,9 +1134,9 @@ public class RestfulServer extends HttpServlet { if (bin.getContent() == null || bin.getContent().length == 0) { return; } - + theHttpResponse.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); - + theHttpResponse.setContentLength(bin.getContent().length); ServletOutputStream oos = theHttpResponse.getOutputStream(); oos.write(bin.getContent()); @@ -1190,6 +1202,4 @@ public class RestfulServer extends HttpServlet { } } - - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index 67c35af67b4..1aa4144ff21 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -143,7 +143,7 @@ public class FhirTerser { case COMPOSITE_DATATYPE: case RESOURCE: { BaseRuntimeElementCompositeDefinition childDef = (BaseRuntimeElementCompositeDefinition) theDefinition; - for (BaseRuntimeChildDefinition nextChild : childDef.getChildren()) { + for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { List values = nextChild.getAccessor().getValues(theElement); if (values != null) { for (IElement nextValue : values) { diff --git a/hapi-fhir-base/src/site/xdoc/doc_cors.xml b/hapi-fhir-base/src/site/xdoc/doc_cors.xml index 37e7ebc31c9..3aef8ea6edd 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_cors.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_cors.xml @@ -71,7 +71,7 @@ A comma separated list of allowed headers when making a non simple CORS request. cors.allowed.headers - X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers + X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization A comma separated list non-standard response headers that will be exposed to XHR2 object. diff --git a/hapi-fhir-base/src/site/xdoc/doc_resource_references.xml b/hapi-fhir-base/src/site/xdoc/doc_resource_references.xml index 4108d77e2a0..cffe514f28e 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_resource_references.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_resource_references.xml @@ -70,23 +70,21 @@

- In server code, you may wish to return "contained" resources. A simple way to do this - is to add these resources directly to the reference field and ensure that the resource - has no ID populated. When this condition is detected, HAPI will automatically create a local - reference ID and add the resource to the "contained" section when it encodes the resource. + In server code, you will often want to return a resource which contains + a link to another resource. Generally these "linked" resources are + not actually included in the response, but rather a link to the + resource is included and the client may request that resource directly + (by ID) if it is needed.

The following example shows a Patient resource being created which will have a - contained resource as its managing organization when encoded from a server: + link to its managing organization when encoded from a server:

- +patient.getManagingOrganization().setReference("Organization/124362");]]> @@ -111,6 +109,21 @@ patient.getManagingOrganization().setResource(patient);]]> org.setId("Organization/65546"); org.getName().setValue("Contained Test Organization"); +Patient patient = new Patient(); +patient.setId("Patient/1333"); +patient.addIdentifier("urn:mrns", "253345"); +patient.getManagingOrganization().setResource(patient);]]> + +

+ On the other hand, if the linked resource + does not have an ID set, the linked resource will + be included in the returned bundle as a "contained" resource. In this + case, HAPI itself will define a local reference ID (e.g. "#001"). +

+ capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(httpClient.execute(capt.capture())).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_ATOM_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(createLinkedBundle()), Charset.forName("UTF-8"))); + + IGenericClient client = ctx.newRestfulGenericClient( "http://foo"); + Bundle bundle = client.search().forResource(IncludeTest.ExtPatient.class).execute(); + + assertEquals(HttpGet.class, capt.getValue().getClass()); + HttpGet get = (HttpGet) capt.getValue(); + assertEquals("http://foo/Patient", get.getURI().toString()); + + assertEquals(4, bundle.size()); + + ExtPatient p = (ExtPatient) bundle.getEntries().get(0).getResource(); + ResourceReferenceDt ref = (ResourceReferenceDt) p.getSecondOrg(); + assertEquals("Organization/o1", ref.getReference().getValue()); + assertNotNull(ref.getResource()); + + Organization o1 = (Organization) ref.getResource(); + assertEquals("o2", o1.getPartOf().getReference().toUnqualifiedVersionless().getIdPart()); + assertNotNull(o1.getPartOf().getResource()); + + } + + private String createLinkedBundle() { + //@formatter:off + return "\n" + + " \n" + + " <id>6cfcd90e-877a-40c6-a11c-448006712979</id>\n" + + " <link rel=\"self\" href=\"http://localhost:49782/Patient?_query=declaredExtInclude&_pretty=true\"/>\n" + + " <link rel=\"fhir-base\" href=\"http://localhost:49782\"/>\n" + + " <os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">2</os:totalResults>\n" + + " <published>2014-08-12T10:22:19.097-04:00</published>\n" + + " <author>\n" + + " <name>HAPI FHIR Server</name>\n" + + " </author>\n" + + " <entry>\n" + + " <title>Patient p1\n" + + " http://localhost:49782/Patient/p1\n" + + " 2014-08-12T10:22:19-04:00\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Patient p2\n" + + " http://localhost:49782/Patient/p2\n" + + " 2014-08-12T10:22:19-04:00\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Organization o1\n" + + " http://localhost:49782/Organization/o1\n" + + " 2014-08-12T10:22:19-04:00\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Organization o2\n" + + " http://localhost:49782/Organization/o2\n" + + " 2014-08-12T10:22:19-04:00\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + //@formatter:on + } + + private String createBundle() { //@formatter:on return "\n" + diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java index 420c7f3b4cc..264ba048502 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/IncludeTest.java @@ -25,6 +25,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu.resource.Organization; import ca.uhn.fhir.model.dstu.resource.Patient; @@ -34,6 +37,7 @@ import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.testutil.RandomServerPortProvider; +import ca.uhn.fhir.util.ElementUtil; /** * Created by dsotnikov on 2/25/2014. @@ -157,6 +161,34 @@ public class IncludeTest { } + + @Test + public void testIIncludedResourcesNonContainedInDeclaredExtension() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=declaredExtInclude&_pretty=true"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); + + ourLog.info(responseContent); + + assertEquals(4, bundle.size()); + assertEquals(new IdDt("Patient/p1"), bundle.toListOfResources().get(0).getId().toUnqualifiedVersionless()); + assertEquals(new IdDt("Patient/p2"), bundle.toListOfResources().get(1).getId().toUnqualifiedVersionless()); + assertEquals(new IdDt("Organization/o1"), bundle.toListOfResources().get(2).getId().toUnqualifiedVersionless()); + assertEquals(new IdDt("Organization/o2"), bundle.toListOfResources().get(3).getId().toUnqualifiedVersionless()); + + Patient p1 = (Patient) bundle.toListOfResources().get(0); + assertEquals(0,p1.getContained().getContainedResources().size()); + + Patient p2 = (Patient) bundle.toListOfResources().get(1); + assertEquals(0,p2.getContained().getContainedResources().size()); + + + } + @Test @@ -214,6 +246,32 @@ public class IncludeTest { } + @ResourceDef(name="Patient") + public static class ExtPatient extends Patient + { + @Child(name="secondOrg") + @Extension(url="http://foo#secondOrg", definedLocally = false, isModifier = false) + private ResourceReferenceDt mySecondOrg; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(mySecondOrg); + } + + public ResourceReferenceDt getSecondOrg() { + if (mySecondOrg==null) { + mySecondOrg= new ResourceReferenceDt(); + } + return mySecondOrg; + } + + public void setSecondOrg(ResourceReferenceDt theSecondOrg) { + mySecondOrg = theSecondOrg; + } + + } + + /** * Created by dsotnikov on 2/25/2014. */ @@ -238,6 +296,7 @@ public class IncludeTest { return Arrays.asList(p1, p2); } + @Search(queryName = "extInclude") public List extInclude() { Organization o1 = new Organization(); @@ -257,6 +316,31 @@ public class IncludeTest { return Arrays.asList(p1, p2); } + @Search(queryName = "declaredExtInclude") + public List declaredExtInclude() { + Organization o1 = new Organization(); + o1.getName().setValue("o1"); + o1.setId("o1"); + + Organization o2 = new Organization(); + o2.getName().setValue("o2"); + o2.setId("o2"); + o1.getPartOf().setResource(o2); + + ExtPatient p1 = new ExtPatient(); + p1.setId("p1"); + p1.addIdentifier().setLabel("p1"); + p1.getSecondOrg().setResource(o1); + + ExtPatient p2 = new ExtPatient(); + p2.setId("p2"); + p2.addIdentifier().setLabel("p2"); + p2.getSecondOrg().setResource(o1); + + return Arrays.asList(p1, p2); + } + + @Search(queryName = "containedInclude") public List containedInclude() { Organization o1 = new Organization(); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component index ee477891317..c94187882b4 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component +++ b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component @@ -12,7 +12,7 @@ uses - + consumes diff --git a/restful-server-example/.settings/org.eclipse.wst.common.component b/restful-server-example/.settings/org.eclipse.wst.common.component index e2cd387d6d8..263e87f0f0d 100644 --- a/restful-server-example/.settings/org.eclipse.wst.common.component +++ b/restful-server-example/.settings/org.eclipse.wst.common.component @@ -6,7 +6,7 @@ uses - + consumes