Linked resources in server now correctly returned

This commit is contained in:
James Agnew 2014-08-12 11:04:41 -04:00
parent 9ef670aae9
commit d0edc03f14
23 changed files with 434 additions and 61 deletions

View File

@ -49,7 +49,7 @@
</action>
<action type="add">
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.
</action>
<action type="add">
@ -67,6 +67,11 @@
<action type="add">
Added narrative generator template for OperationOutcome resource
</action>
<action type="fix">
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!
</action>
</release>
<release version="0.5" date="2014-Jul-30">
<action type="add">

View File

@ -34,6 +34,7 @@ import ca.uhn.fhir.parser.DataFormatException;
public abstract class BaseRuntimeElementCompositeDefinition<T extends ICompositeElement> extends BaseRuntimeElementDefinition<T> {
private List<BaseRuntimeChildDefinition> myChildren = new ArrayList<BaseRuntimeChildDefinition>();
private List<BaseRuntimeChildDefinition> myChildrenAndExtensions;
private Map<String, BaseRuntimeChildDefinition> myNameToChild = new HashMap<String, BaseRuntimeChildDefinition>();
public BaseRuntimeElementCompositeDefinition(String theName, Class<? extends T> theImplementingClass) {
@ -65,6 +66,10 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IComposite
return myChildren;
}
public List<BaseRuntimeChildDefinition> getChildrenAndExtension() {
return myChildrenAndExtensions;
}
@Override
public void sealAndInitialize(Map<Class<? extends IElement>, BaseRuntimeElementDefinition<?>> theClassToElementDefinitions) {
super.sealAndInitialize(theClassToElementDefinitions);
@ -90,5 +95,12 @@ public abstract class BaseRuntimeElementCompositeDefinition<T extends IComposite
myChildren = Collections.unmodifiableList(myChildren);
myNameToChild = Collections.unmodifiableMap(myNameToChild);
List<BaseRuntimeChildDefinition> children = new ArrayList<BaseRuntimeChildDefinition>();
children.addAll(myChildren);
children.addAll(getExtensionsModifier());
children.addAll(getExtensionsNonModifier());
myChildrenAndExtensions=Collections.unmodifiableList(children);
}
}

View File

@ -179,7 +179,7 @@ public class FhirContext {
* Create and return a new JSON parser.
*
* <p>
* Performance Note: <b>This class is cheap</b> to create, and may be called once for every message being processed without incurring any performance penalty
* Performance Note: <b>This method is cheap</b> to call, and may be called once for every message being processed without incurring any performance penalty
* </p>
*/
public IParser newJsonParser() {
@ -192,7 +192,7 @@ public class FhirContext {
* href="http://hl7api.sourceforge.net/hapi-fhir/doc_rest_client.html">RESTful Client</a> documentation for more information on how to define this interface.
*
* <p>
* Performance Note: <b>This class is cheap</b> to create, and may be called once for every message being processed without incurring any performance penalty
* Performance Note: <b>This method is cheap</b> to call, and may be called once for every operation invocation without incurring any performance penalty
* </p>
*
* @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}).
*
* <p>
* In most cases it is preferable to use the non-generic clients instead of this mechanism, but not always.
* Performance Note: <b>This method is cheap</b> to call, and may be called once for every operation invocation without incurring any performance penalty
* </p>
*
* @param theServerBase
@ -227,10 +228,10 @@ public class FhirContext {
}
/**
* Create and return a new JSON parser.
* Create and return a new XML parser.
*
* <p>
* Performance Note: <b>This class is cheap</b> to create, and may be called once for every message being processed without incurring any performance penalty
* Performance Note: <b>This method is cheap</b> to call, and may be called once for every message being processed without incurring any performance penalty
* </p>
*/
public IParser newXmlParser() {

View File

@ -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 */{

View File

@ -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<Date> {
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<Date> {
} 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<Date> {
} 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));

View File

@ -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 extends IResource> T parseResource(Class<T> 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;
/**

View File

@ -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;
}
}
}

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class DateParam extends DateTimeDt implements IQueryParameterType, IQueryParameterOr<DateParam> {
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();
}

View File

@ -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<QuantityCompararatorEnum>) 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();
}

View File

@ -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)) {

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;

View File

@ -581,13 +581,13 @@ public class RestfulServer extends HttpServlet {
*/
private int escapedLength(String theServletPath) {
int delta = 0;
for(int i =0;i<theServletPath.length();i++) {
for (int i = 0; i < theServletPath.length(); i++) {
char next = theServletPath.charAt(i);
if (next == ' ') {
delta = delta + 2;
}
}
return theServletPath.length()+delta;
return theServletPath.length() + delta;
}
/**
@ -812,34 +812,47 @@ public class RestfulServer extends HttpServlet {
}
List<ResourceReferenceDt> 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<IResource> addedResourcesThisPass = new ArrayList<IResource>();
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<ResourceReferenceDt>();
for (IResource iResource : addedResourcesThisPass) {
List<ResourceReferenceDt> 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 {
}
}
}

View File

@ -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<? extends IElement> values = nextChild.getAccessor().getValues(theElement);
if (values != null) {
for (IElement nextValue : values) {

View File

@ -71,7 +71,7 @@
<init-param>
<description>A comma separated list of allowed headers when making a non simple CORS request.</description>
<param-name>cors.allowed.headers</param-name>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
<param-value>X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization</param-value>
</init-param>
<init-param>
<description>A comma separated list non-standard response headers that will be exposed to XHR2 object.</description>

View File

@ -70,23 +70,21 @@
<section name="References in Server Code">
<p>
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.
</p>
<p>
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:
</p>
<source><![CDATA[Organization containedOrg = new Organization();
containedOrg.getName().setValue("Contained Test Organization");
Patient patient = new Patient();
<source><![CDATA[Patient patient = new Patient();
patient.setId("Patient/1333");
patient.addIdentifier("urn:mrns", "253345");
patient.getManagingOrganization().setResource(patient);]]></source>
patient.getManagingOrganization().setReference("Organization/124362");]]></source>
<subsection name="Handling Includes (_include) in a Bundle">
@ -111,6 +109,21 @@ patient.getManagingOrganization().setResource(patient);]]></source>
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);]]></source>
<p>
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").
</p>
<source><![CDATA[Organization org = new Organization();
// org.setId("Organization/65546");
org.getName().setValue("Contained Test Organization");
Patient patient = new Patient();
patient.setId("Patient/1333");
patient.addIdentifier("urn:mrns", "253345");

View File

@ -34,6 +34,12 @@ public class BaseDateTimeDtTest {
assertEquals(TemporalPrecisionEnum.YEAR, dt.getPrecision());
}
@Test()
public void testParseMalformatted() throws DataFormatException {
DateTimeDt dt = new DateTimeDt("20120102");
assertEquals("2012-01-02",dt.getValueAsString());
}
@Test
public void testParseMonth() throws DataFormatException {
DateTimeDt dt = new DateTimeDt();

View File

@ -35,7 +35,6 @@ public class IdDtTest {
public void testBigDecimalIds() {
IdDt id = new IdDt(new BigDecimal("123"));
assertEquals(id.asBigDecimal(), new BigDecimal("123"));
assertEquals(id.getIdPartAsBigDecimal(), new BigDecimal("123"));
}

View File

@ -25,10 +25,14 @@ import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IncludeTest;
import ca.uhn.fhir.rest.server.IncludeTest.ExtPatient;
public class IncludedResourceStitchingClientTest {
@ -75,6 +79,113 @@ public class IncludedResourceStitchingClientTest {
}
@Test
public void testWithDeclaredExtension() throws Exception {
ArgumentCaptor<HttpUriRequest> 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 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" +
" <title/>\n" +
" <id>6cfcd90e-877a-40c6-a11c-448006712979</id>\n" +
" <link rel=\"self\" href=\"http://localhost:49782/Patient?_query=declaredExtInclude&amp;_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</title>\n" +
" <id>http://localhost:49782/Patient/p1</id>\n" +
" <published>2014-08-12T10:22:19-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49782/Patient/p1\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Patient xmlns=\"http://hl7.org/fhir\">\n" +
" <extension url=\"http://foo#secondOrg\">\n" +
" <valueResource>\n" +
" <reference value=\"Organization/o1\"/>\n" +
" </valueResource>\n" +
" </extension>\n" +
" <identifier>\n" +
" <label value=\"p1\"/>\n" +
" </identifier>\n" +
" </Patient>\n" +
" </content>\n" +
" </entry>\n" +
" <entry>\n" +
" <title>Patient p2</title>\n" +
" <id>http://localhost:49782/Patient/p2</id>\n" +
" <published>2014-08-12T10:22:19-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49782/Patient/p2\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Patient xmlns=\"http://hl7.org/fhir\">\n" +
" <extension url=\"http://foo#secondOrg\">\n" +
" <valueResource>\n" +
" <reference value=\"Organization/o1\"/>\n" +
" </valueResource>\n" +
" </extension>\n" +
" <identifier>\n" +
" <label value=\"p2\"/>\n" +
" </identifier>\n" +
" </Patient>\n" +
" </content>\n" +
" </entry>\n" +
" <entry>\n" +
" <title>Organization o1</title>\n" +
" <id>http://localhost:49782/Organization/o1</id>\n" +
" <published>2014-08-12T10:22:19-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49782/Organization/o1\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Organization xmlns=\"http://hl7.org/fhir\">\n" +
" <name value=\"o1\"/>\n" +
" <partOf>\n" +
" <reference value=\"Organization/o2\"/>\n" +
" </partOf>\n" +
" </Organization>\n" +
" </content>\n" +
" </entry>\n" +
" <entry>\n" +
" <title>Organization o2</title>\n" +
" <id>http://localhost:49782/Organization/o2</id>\n" +
" <published>2014-08-12T10:22:19-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49782/Organization/o2\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Organization xmlns=\"http://hl7.org/fhir\">\n" +
" <name value=\"o2\"/>\n" +
" </Organization>\n" +
" </content>\n" +
" </entry>\n" +
"</feed>";
//@formatter:on
}
private String createBundle() {
//@formatter:on
return "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" +

View File

@ -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<Patient> extInclude() {
Organization o1 = new Organization();
@ -257,6 +316,31 @@ public class IncludeTest {
return Arrays.asList(p1, p2);
}
@Search(queryName = "declaredExtInclude")
public List<ExtPatient> 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<Patient> containedInclude() {
Organization o1 = new Organization();

View File

@ -12,7 +12,7 @@
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/var/M2_REPO/ca/uhn/hapi/fhir/hapi-fhir-testpage-overlay/0.6-SNAPSHOT/hapi-fhir-testpage-overlay-0.6-SNAPSHOT.war?unpackFolder=target/m2e-wtp/overlays&amp;includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependency-type>consumes</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">

View File

@ -6,7 +6,7 @@
<dependent-module archiveName="hapi-fhir-base-0.6-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/var/M2_REPO/ca/uhn/hapi/fhir/hapi-fhir-testpage-overlay/0.6-SNAPSHOT/hapi-fhir-testpage-overlay-0.6-SNAPSHOT.war?unpackFolder=target/m2e-wtp/overlays&amp;includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">
<dependency-type>consumes</dependency-type>
</dependent-module>
<dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&amp;excludes=META-INF/MANIFEST.MF">