diff --git a/examples/src/main/java/example/ExtensionsDstu3.java b/examples/src/main/java/example/ExtensionsDstu3.java index 01df6c3fd56..664b9526d92 100644 --- a/examples/src/main/java/example/ExtensionsDstu3.java +++ b/examples/src/main/java/example/ExtensionsDstu3.java @@ -3,14 +3,17 @@ package example; import java.io.IOException; import java.util.List; +import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.DateTimeType; import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.HumanName; +import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.Identifier.IdentifierUse; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.StringType; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.CustomTypeDstu3Test.MyCustomPatient; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.client.IGenericClient; @@ -33,7 +36,31 @@ client.create().resource(custPatient).execute(); custPatient = client.read().resource(MyPatient.class).withId("123").execute(); //END SNIPPET: customTypeClientSimple +//START SNIPPET: customTypeClientSearch +// Perform the search using the custom type +Bundle bundle = client + .search() + .forResource(MyPatient.class) + .returnBundle(Bundle.class) + .execute(); + +// Entries in the return bundle will use the given type +MyPatient pat0 = (MyPatient) bundle.getEntry().get(0).getResource(); +//END SNIPPET: customTypeClientSearch +//START SNIPPET: customTypeClientSearch2 +//Perform the search using the custom type +bundle = client + .history() + .onInstance(new IdType("Patient/123")) + .andReturnBundle(Bundle.class) + .preferResponseType(MyPatient.class) + .execute(); + +//Entries in the return bundle will use the given type +MyPatient historyPatient0 = (MyPatient) bundle.getEntry().get(0).getResource(); +//END SNIPPET: customTypeClientSearch2 + } public void customTypeDeclared() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index 61852608ab9..0d1569e0820 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -88,6 +88,7 @@ public abstract class BaseParser implements IParser { private IIdType myEncodeForceResourceId; private IParserErrorHandler myErrorHandler; private boolean myOmitResourceId; + private List> myPreferTypes; private String myServerBaseUrl; private boolean myStripVersionsFromReferences = true; private boolean mySummaryMode; @@ -437,6 +438,11 @@ public abstract class BaseParser implements IParser { return tags; } + @Override + public List> getPreferTypes() { + return myPreferTypes; + } + @SuppressWarnings("deprecation") protected > List getProfileTagsForEncoding(IBaseResource theResource, List theProfiles) { switch (myContext.getAddProfileTagWhenEncoding()) { @@ -717,6 +723,11 @@ public abstract class BaseParser implements IParser { return this; } + @Override + public void setPreferTypes(List> thePreferTypes) { + myPreferTypes = thePreferTypes; + } + @Override public IParser setServerBaseUrl(String theUrl) { myServerBaseUrl = isNotBlank(theUrl) ? theUrl : null; 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 fc1a0b69fc0..215497bb5e1 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 @@ -23,6 +23,7 @@ package ca.uhn.fhir.parser; import java.io.IOException; import java.io.Reader; import java.io.Writer; +import java.util.List; import java.util.Set; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -30,6 +31,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.context.ConfigurationException; +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.TagList; @@ -62,7 +64,7 @@ public interface IParser { * @return An encoded tag list */ String encodeTagListToString(TagList theTagList); - + /** * Encodes a tag list, as defined in the FHIR * Specification. @@ -73,7 +75,7 @@ public interface IParser { * The writer to encode to */ void encodeTagListToWriter(TagList theTagList, Writer theWriter) throws IOException; - + /** * See {@link #setEncodeElements(Set)} */ @@ -95,6 +97,14 @@ public interface IParser { */ EncodingEnum getEncoding(); + /** + * Gets the preferred types, as set using {@link #setPreferTypes(List)} + * + * @return Returns the preferred types, or null + * @see #setPreferTypes(List) + */ + List> getPreferTypes(); + /** * Returns true if resource IDs should be omitted * @@ -289,6 +299,25 @@ public interface IParser { */ IParser setParserErrorHandler(IParserErrorHandler theErrorHandler); + /** + * If set, when parsing resources the parser will try to use the given types when possible, in + * the order that they are provided (from highest to lowest priority). For example, if a custom + * type which declares to implement the Patient resource is passed in here, and the + * parser is parsing a Bundle containing a Patient resource, the parser will use the given + * custom type. + *

+ * This feature is related to, but not the same as the + * {@link FhirContext#setDefaultTypeForProfile(String, Class)} feature. + * setDefaultTypeForProfile is used to specify a type to be used + * when a resource explicitly declares support for a given profile. This + * feature specifies a type to be used irrespective of the profile declaration + * in the metadata statement. + *

+ * + * @param thePreferTypes The preferred types, or null + */ + void setPreferTypes(List> thePreferTypes); + /** * Sets the "pretty print" flag, meaning that the parser will encode resources with human-readable spacing and * newlines between elements instead of condensing output as much as possible. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 7522f4b17f3..b45add38dc1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -220,7 +220,7 @@ public class JsonParser extends BaseParser implements IParser { assertObjectOfType(resourceTypeObj, JsonValue.ValueType.STRING, "resourceType"); String resourceType = ((JsonString) resourceTypeObj).getString(); - ParserState state = ParserState.getPreResourceInstance(theResourceType, myContext, true, getErrorHandler()); + ParserState state = ParserState.getPreResourceInstance(this, theResourceType, myContext, true, getErrorHandler()); state.enteringNewElement(null, resourceType); parseChildren(object, state); @@ -1097,7 +1097,7 @@ public class JsonParser extends BaseParser implements IParser { throw new DataFormatException("Trying to parse bundle but found resourceType other than 'Bundle'. Found: '" + resourceType + "'"); } - ParserState state = ParserState.getPreAtomInstance(myContext, theResourceType, true, getErrorHandler()); + ParserState state = ParserState.getPreAtomInstance(this, myContext, theResourceType, true, getErrorHandler()); if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) { state.enteringNewElement(null, "Bundle"); } else { @@ -1420,7 +1420,7 @@ public class JsonParser extends BaseParser implements IParser { assertObjectOfType(resourceTypeObj, JsonValue.ValueType.STRING, "resourceType"); String resourceType = ((JsonString) resourceTypeObj).getString(); - ParserState state = ParserState.getPreTagListInstance(myContext, true, getErrorHandler()); + ParserState state = ParserState.getPreTagListInstance(this, myContext, true, getErrorHandler()); state.enteringNewElement(null, resourceType); parseChildren(object, state); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java index 12504caaf71..8a589241ce8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java @@ -91,14 +91,16 @@ class ParserState { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ParserState.class); private List myComments = new ArrayList(2); - private FhirContext myContext; - private IParserErrorHandler myErrorHandler; - private boolean myJsonMode; + private final FhirContext myContext; + private final IParserErrorHandler myErrorHandler; + private final boolean myJsonMode; private T myObject; private BaseState myState; private IBase myPreviousElement; + private final IParser myParser; - private ParserState(FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) { + private ParserState(IParser theParser, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) { + myParser = theParser; myContext = theContext; myJsonMode = theJsonMode; myErrorHandler = theErrorHandler; @@ -247,8 +249,8 @@ class ParserState { myState.xmlEvent(theNextEvent); } - static ParserState getPreAtomInstance(FhirContext theContext, Class theResourceType, boolean theJsonMode, IParserErrorHandler theErrorHandler) throws DataFormatException { - ParserState retVal = new ParserState(theContext, theJsonMode, theErrorHandler); + static ParserState getPreAtomInstance(IParser theParser, FhirContext theContext, Class theResourceType, boolean theJsonMode, IParserErrorHandler theErrorHandler) throws DataFormatException { + ParserState retVal = new ParserState(theParser, theContext, theJsonMode, theErrorHandler); if (theContext.getVersion().getVersion() == FhirVersionEnum.DSTU1) { retVal.push(retVal.new PreAtomState(theResourceType)); } else { @@ -261,8 +263,8 @@ class ParserState { * @param theResourceType * May be null */ - static ParserState getPreResourceInstance(Class theResourceType, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) throws DataFormatException { - ParserState retVal = new ParserState(theContext, theJsonMode, theErrorHandler); + static ParserState getPreResourceInstance(IParser theParser, Class theResourceType, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) throws DataFormatException { + ParserState retVal = new ParserState(theParser, theContext, theJsonMode, theErrorHandler); if (theResourceType == null) { if (theContext.getVersion().getVersion().isRi()) { retVal.push(retVal.new PreResourceStateHl7Org(theResourceType)); @@ -279,8 +281,8 @@ class ParserState { return retVal; } - static ParserState getPreTagListInstance(FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) { - ParserState retVal = new ParserState(theContext, theJsonMode, theErrorHandler); + static ParserState getPreTagListInstance(IParser theParser, FhirContext theContext, boolean theJsonMode, IParserErrorHandler theErrorHandler) { + ParserState retVal = new ParserState(theParser, theContext, theJsonMode, theErrorHandler); retVal.push(retVal.new PreTagListState()); return retVal; } @@ -1997,7 +1999,18 @@ class ParserState { public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException { BaseRuntimeElementDefinition definition; if (myResourceType == null) { - definition = myContext.getResourceDefinition(myParentVersion, theLocalPart); + definition = null; + if (myParser.getPreferTypes() != null) { + for (Class next : myParser.getPreferTypes()) { + RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(next); + if (nextDef.getName().equals(theLocalPart)) { + definition = nextDef; + } + } + } + if (definition == null) { + definition = myContext.getResourceDefinition(myParentVersion, theLocalPart); + } if ((definition == null)) { throw new DataFormatException("Element '" + theLocalPart + "' is not a known resource type, expected a resource at this position"); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java index aebe2ad0d1f..45e18fe214c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java @@ -1080,12 +1080,12 @@ public class XmlParser extends BaseParser implements IParser { } private Bundle parseBundle(XMLEventReader theStreamReader, Class theResourceType) { - ParserState parserState = ParserState.getPreAtomInstance(myContext, theResourceType, false, getErrorHandler()); + ParserState parserState = ParserState.getPreAtomInstance(this, myContext, theResourceType, false, getErrorHandler()); return doXmlLoop(theStreamReader, parserState); } private T parseResource(Class theResourceType, XMLEventReader theStreamReader) { - ParserState parserState = ParserState.getPreResourceInstance(theResourceType, myContext, false, getErrorHandler()); + ParserState parserState = ParserState.getPreResourceInstance(this, theResourceType, myContext, false, getErrorHandler()); return doXmlLoop(theStreamReader, parserState); } @@ -1093,7 +1093,7 @@ public class XmlParser extends BaseParser implements IParser { public TagList parseTagList(Reader theReader) { XMLEventReader streamReader = createStreamReader(theReader); - ParserState parserState = ParserState.getPreTagListInstance(myContext, false, getErrorHandler()); + ParserState parserState = ParserState.getPreTagListInstance(this, myContext, false, getErrorHandler()); return doXmlLoop(streamReader, parserState); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java index 74adf164721..2a7f1a44258 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java @@ -107,6 +107,13 @@ public abstract class BaseClient implements IRestfulClient { return retVal; } + @Override + public T fetchResourceFromUrl(Class theResourceType, String theUrl) { + BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl); + ResourceResponseHandler binding = new ResourceResponseHandler(theResourceType); + return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null); + } + void forceConformanceCheck() { myFactory.validateServerBase(myUrlBase, myClient, this); } @@ -189,39 +196,39 @@ public abstract class BaseClient implements IRestfulClient { IHttpRequest httpRequest; IHttpResponse response = null; try { - Map> params = createExtraParams(); + Map> params = createExtraParams(); - if (theEncoding == EncodingEnum.XML) { - params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); - } else if (theEncoding == EncodingEnum.JSON) { - params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); - } + if (theEncoding == EncodingEnum.XML) { + params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); + } else if (theEncoding == EncodingEnum.JSON) { + params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + } - if (theSummaryMode != null) { - params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode())); - } else if (mySummary != null) { - params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode())); - } + if (theSummaryMode != null) { + params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode())); + } else if (mySummary != null) { + params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode())); + } - if (thePrettyPrint == Boolean.TRUE) { - params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE)); - } + if (thePrettyPrint == Boolean.TRUE) { + params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE)); + } - if (theSubsetElements != null && theSubsetElements.isEmpty() == false) { - params.put(Constants.PARAM_ELEMENTS, Collections.singletonList(StringUtils.join(theSubsetElements, ','))); - } + if (theSubsetElements != null && theSubsetElements.isEmpty() == false) { + params.put(Constants.PARAM_ELEMENTS, Collections.singletonList(StringUtils.join(theSubsetElements, ','))); + } - EncodingEnum encoding = getEncoding(); - if (theEncoding != null) { - encoding = theEncoding; - } + EncodingEnum encoding = getEncoding(); + if (theEncoding != null) { + encoding = theEncoding; + } httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint); if (theLogRequestAndResponse) { ourLog.info("Client invoking: {}", httpRequest); String body = httpRequest.getRequestBodyFromStream(); - if(body != null) { + if (body != null) { ourLog.info("Client request body: {}", body); } } @@ -337,7 +344,7 @@ public abstract class BaseClient implements IRestfulClient { throw new FhirClientConnectionException(e); } catch (IOException e) { throw new FhirClientConnectionException(e); - } catch(RuntimeException e) { + } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new FhirClientConnectionException(e); @@ -449,28 +456,47 @@ public abstract class BaseClient implements IRestfulClient { Validate.notNull(theInterceptor, "Interceptor can not be null"); myInterceptors.remove(theInterceptor); } - - @Override - public T fetchResourceFromUrl(Class theResourceType, String theUrl) { - BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl); - ResourceResponseHandler binding = new ResourceResponseHandler(theResourceType, null, false); - return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null); + static ArrayList> toTypeList(Class thePreferResponseType) { + ArrayList> preferResponseTypes = null; + if (thePreferResponseType != null) { + preferResponseTypes = new ArrayList>(1); + preferResponseTypes.add(thePreferResponseType); + } + return preferResponseTypes; } protected final class ResourceResponseHandler implements IClientResponseHandler { private boolean myAllowHtmlResponse; private IIdType myId; - private Class myType; + private List> myPreferResponseTypes; + private Class myReturnType; - public ResourceResponseHandler(Class theType, IIdType theId) { - myType = theType; - myId = theId; + public ResourceResponseHandler() { + this(null); } - public ResourceResponseHandler(Class theType, IIdType theId, boolean theAllowHtmlResponse) { - myType = theType; + public ResourceResponseHandler(Class theReturnType) { + this(theReturnType, null, null); + } + + public ResourceResponseHandler(Class theReturnType, Class thePreferResponseType, IIdType theId) { + this(theReturnType, thePreferResponseType, theId, false); + } + + public ResourceResponseHandler(Class theReturnType, Class thePreferResponseType, IIdType theId, boolean theAllowHtmlResponse) { + this(theReturnType, toTypeList(thePreferResponseType), theId, theAllowHtmlResponse); + } + + public ResourceResponseHandler(Class theClass, List> thePreferResponseTypes) { + this(theClass, thePreferResponseTypes, null, false); + } + + + public ResourceResponseHandler(Class theReturnType, List> thePreferResponseTypes, IIdType theId, boolean theAllowHtmlResponse) { + myReturnType = theReturnType; myId = theId; + myPreferResponseTypes = thePreferResponseTypes; myAllowHtmlResponse = theAllowHtmlResponse; } @@ -478,13 +504,16 @@ public abstract class BaseClient implements IRestfulClient { public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { - if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myType != null) { + if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myReturnType != null) { return readHtmlResponse(theResponseReader); } throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); } IParser parser = respType.newParser(getFhirContext()); - T retVal = parser.parseResource(myType, theResponseReader); + if (myPreferResponseTypes != null) { + parser.setPreferTypes(myPreferResponseTypes); + } + T retVal = parser.parseResource(myReturnType, theResponseReader); MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal); @@ -493,7 +522,7 @@ public abstract class BaseClient implements IRestfulClient { @SuppressWarnings("unchecked") private T readHtmlResponse(Reader theResponseReader) { - RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myType); + RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType); IBaseResource instance = resDef.newInstance(); BaseRuntimeChildDefinition textChild = resDef.getChildByName("text"); BaseRuntimeElementCompositeDefinition textElement = (BaseRuntimeElementCompositeDefinition) textChild.getChildByName("text"); @@ -511,6 +540,10 @@ public abstract class BaseClient implements IRestfulClient { divChild.getMutator().addValue(textInstance, divInstance); return (T) instance; } + + public void setPreferResponseTypes(List> thePreferResponseTypes) { + myPreferResponseTypes = thePreferResponseTypes; + } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index 578041c9935..c0454e24838 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -182,7 +182,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") Class conformance = (Class) myContext.getResourceDefinition("Conformance").getImplementingClass(); - ResourceResponseHandler binding = new ResourceResponseHandler(conformance, null); + ResourceResponseHandler binding = new ResourceResponseHandler(conformance); BaseConformance resp = invokeClient(myContext, binding, invocation, myLogRequestAndResponse); return resp; } @@ -263,7 +263,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } boolean allowHtmlResponse = (theSummary == SummaryEnum.TEXT) || (theSummary == null && getSummary() == SummaryEnum.TEXT); - ResourceResponseHandler binding = new ResourceResponseHandler(theType, id, allowHtmlResponse); + ResourceResponseHandler binding = new ResourceResponseHandler(theType, (Class)null, id, allowHtmlResponse); if (theNotModifiedHandler == null) { return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements); @@ -596,6 +596,39 @@ public class GenericClient extends BaseClient implements IGenericClient { } private abstract class BaseClientExecutable, Y> implements IClientExecutable { + + private List> myPreferResponseTypes; + + public List> getPreferResponseTypes() { + return myPreferResponseTypes; + } + + public List> getPreferResponseTypes(Class theDefault) { + if (myPreferResponseTypes != null) { + return myPreferResponseTypes; + } else { + return toTypeList(theDefault); + } + } + + @SuppressWarnings("unchecked") + @Override + public T preferResponseType(Class theClass) { + myPreferResponseTypes = null; + if (theClass != null) { + myPreferResponseTypes = new ArrayList>(); + myPreferResponseTypes.add(theClass); + } + return (T) this; + } + + @SuppressWarnings("unchecked") + @Override + public T preferResponseTypes(List> theClass) { + myPreferResponseTypes = theClass; + return (T) this; + } + protected EncodingEnum myParamEncoding; protected Boolean myPrettyPrint; private boolean myQueryLogRequestAndResponse; @@ -925,7 +958,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public Object execute() { - ResourceResponseHandler binding = new ResourceResponseHandler(myType.getImplementingClass(), null); + ResourceResponseHandler binding = new ResourceResponseHandler(myType.getImplementingClass()); HttpGetClientInvocation invocation = MethodUtil.createConformanceInvocation(getFhirContext()); return super.invoke(null, binding, invocation); } @@ -963,7 +996,7 @@ public class GenericClient extends BaseClient implements IGenericClient { if (myBundleType == null) { binding = new BundleResponseHandler(null); } else { - binding = new ResourceResponseHandler(myBundleType, null); + binding = new ResourceResponseHandler(myBundleType, getPreferResponseTypes()); } HttpSimpleGetClientInvocation invocation = new HttpSimpleGetClientInvocation(myContext, myUrl); @@ -1087,7 +1120,7 @@ public class GenericClient extends BaseClient implements IGenericClient { IClientResponseHandler handler; if (myReturnType != null) { - handler = new ResourceResponseHandler(myReturnType, null); + handler = new ResourceResponseHandler(myReturnType, getPreferResponseTypes(myType)); } else { handler = new BundleResponseHandler(null); } @@ -1391,9 +1424,9 @@ public class GenericClient extends BaseClient implements IGenericClient { BaseHttpClientInvocation invocation = OperationMethodBinding.createOperationInvocation(myContext, resourceName, id, myOperationName, myParameters, myUseHttpGet); - IClientResponseHandler handler; - handler = new ResourceResponseHandler(null, null); - + ResourceResponseHandler handler = new ResourceResponseHandler(); + handler.setPreferResponseTypes(getPreferResponseTypes(myType)); + Object retVal = invoke(null, handler, invocation); if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) { return retVal; @@ -1705,7 +1738,7 @@ public class GenericClient extends BaseClient implements IGenericClient { throws BaseServerResponseException { if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) { Class bundleType = myContext.getResourceDefinition("Bundle").getImplementingClass(); - ResourceResponseHandler handler = new ResourceResponseHandler((Class) bundleType, null); + ResourceResponseHandler handler = new ResourceResponseHandler((Class) bundleType); IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders); IVersionSpecificBundleFactory bundleFactory = myContext.newBundleFactory(); bundleFactory.initializeWithBundleResource(response); @@ -1804,7 +1837,7 @@ public class GenericClient extends BaseClient implements IGenericClient { IClientResponseHandler binding; if (myReturnBundleType != null) { - binding = new ResourceResponseHandler(myReturnBundleType, null); + binding = new ResourceResponseHandler(myReturnBundleType, getPreferResponseTypes(myResourceType)); } else { binding = new BundleResponseHandler(myResourceType); } @@ -2070,13 +2103,13 @@ public class GenericClient extends BaseClient implements IGenericClient { BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(myResources, myContext); return (T) invoke(params, binding, invocation); } else if (myBaseBundle != null) { - ResourceResponseHandler binding = new ResourceResponseHandler(myBaseBundle.getClass(), null); + ResourceResponseHandler binding = new ResourceResponseHandler(myBaseBundle.getClass(), getPreferResponseTypes()); BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(myBaseBundle, myContext); return (T) invoke(params, binding, invocation); } else if (myRawBundle != null) { StringResponseHandler binding = new StringResponseHandler(); /* - * If the user has explicitly requested a given encoding, we may need to reencode the raw string + * If the user has explicitly requested a given encoding, we may need to re-encode the raw string */ if (getParamEncoding() != null) { if (MethodUtil.detectEncodingNoDefault(myRawBundle) != getParamEncoding()) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java index e9083446e6e..8586f16db91 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java @@ -1,5 +1,9 @@ package ca.uhn.fhir.rest.gclient; +import java.util.List; + +import org.hl7.fhir.instance.model.api.IBaseResource; + import ca.uhn.fhir.rest.api.SummaryEnum; /* @@ -49,6 +53,27 @@ public interface IClientExecutable, Y> { T prettyPrint(); + /** + * Explicitly specify a custom structure type to attempt to use when parsing the response. This + * is useful for invocations where the response is a Bundle/Parameters containing nested resources, + * and you want to use specific custom structures for those nested resources. + *

+ * See Profiles and Extensions for more information on using custom structures + *

+ */ + T preferResponseType(Class theType); + + /** + * Explicitly specify a list of custom structure types to attempt to use (in order from most to + * least preferred) when parsing the response. This + * is useful for invocations where the response is a Bundle/Parameters containing nested resources, + * and you want to use specific custom structures for those nested resources. + *

+ * See Profiles and Extensions for more information on using custom structures + *

+ */ + T preferResponseTypes(List> theTypes); + /** * Request that the server modify the response using the _summary param */ diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu3Test.java index 32c7e47b4b5..08426b17a7f 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu3Test.java @@ -38,7 +38,7 @@ public class CustomTypeDstu3Test { } - private String createBundle(String... theResources) { + public static String createBundle(String... theResources) { StringBuilder b = new StringBuilder(); b.append("\n"); for (String next : theResources) { @@ -52,7 +52,7 @@ public class CustomTypeDstu3Test { return b.toString(); } - private String createResource(boolean theWithProfile) { + public static String createResource(boolean theWithProfile) { StringBuilder b = new StringBuilder(); b.append("\n"); if (theWithProfile) { @@ -120,7 +120,6 @@ public class CustomTypeDstu3Test { @Test public void parseBundleWithResourceDirective() { - String input = createBundle(createResource(false), createResource(true)); FhirContext ctx = FhirContext.forDstu3(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java index f5f72cc730b..45233cbf4ab 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java @@ -10,7 +10,9 @@ import static org.mockito.Mockito.when; import java.io.IOException; import java.io.StringReader; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.ReaderInputStream; @@ -22,9 +24,12 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicStatusLine; import org.hl7.fhir.dstu3.model.Binary; +import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Conformance; import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -34,6 +39,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.CustomTypeDstu3Test; +import ca.uhn.fhir.parser.CustomTypeDstu3Test.MyCustomPatient; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.util.VersionUtil; @@ -88,6 +95,163 @@ public class GenericClientDstu3Test { validateUserAgent(capt); } + @Test + public void testExplicitCustomTypeSearch() throws Exception { + final String respString = CustomTypeDstu3Test.createBundle(CustomTypeDstu3Test.createResource(false)); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + //@formatter:off + Bundle resp = client + .search() + .forResource(CustomTypeDstu3Test.MyCustomPatient.class) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + + assertEquals(1, resp.getEntry().size()); + assertEquals(CustomTypeDstu3Test.MyCustomPatient.class, resp.getEntry().get(0).getResource().getClass()); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); + } + + @Test + public void testExplicitCustomTypeHistoryType() throws Exception { + final String respString = CustomTypeDstu3Test.createBundle(CustomTypeDstu3Test.createResource(false)); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + //@formatter:off + Bundle resp = client + .history() + .onType(CustomTypeDstu3Test.MyCustomPatient.class) + .andReturnBundle(Bundle.class) + .execute(); + //@formatter:on + + assertEquals(1, resp.getEntry().size()); + assertEquals(CustomTypeDstu3Test.MyCustomPatient.class, resp.getEntry().get(0).getResource().getClass()); + assertEquals("http://example.com/fhir/Patient/_history", capt.getAllValues().get(0).getURI().toASCIIString()); + } + + @Test + public void testExplicitCustomTypeLoadPage() throws Exception { + final String respString = CustomTypeDstu3Test.createBundle(CustomTypeDstu3Test.createResource(false)); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + Bundle bundle = new Bundle(); + bundle.addLink().setRelation("next").setUrl("http://foo/next"); + + //@formatter:off + Bundle resp = client + .loadPage() + .next(bundle) + .preferResponseType(MyCustomPatient.class) + .execute(); + //@formatter:on + + assertEquals(1, resp.getEntry().size()); + assertEquals(CustomTypeDstu3Test.MyCustomPatient.class, resp.getEntry().get(0).getResource().getClass()); + assertEquals("http://foo/next", capt.getAllValues().get(0).getURI().toASCIIString()); + + //@formatter:off + resp = client + .loadPage() + .next(bundle) + .preferResponseTypes(toTypeList(MyCustomPatient.class)) + .execute(); + //@formatter:on + + assertEquals(1, resp.getEntry().size()); + assertEquals(CustomTypeDstu3Test.MyCustomPatient.class, resp.getEntry().get(0).getResource().getClass()); + assertEquals("http://foo/next", capt.getAllValues().get(0).getURI().toASCIIString()); + } + + @Test + public void testExplicitCustomTypeOperation() throws Exception { + + Parameters param = new Parameters(); + Patient patient = new Patient(); + patient.addName().addFamily("FOO"); + param.addParameter().setName("foo").setResource(patient); + final String respString = ourCtx.newXmlParser().encodeResourceToString(param); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + //@formatter:off + Parameters resp = client + .operation() + .onServer() + .named("foo") + .withNoParameters(Parameters.class) + .preferResponseType(MyCustomPatient.class) + .execute(); + //@formatter:on + + assertEquals(1, resp.getParameter().size()); + assertEquals(CustomTypeDstu3Test.MyCustomPatient.class, resp.getParameter().get(0).getResource().getClass()); + assertEquals("http://example.com/fhir/$foo", capt.getAllValues().get(0).getURI().toASCIIString()); + + //@formatter:off + resp = client + .operation() + .onType(MyCustomPatient.class) + .named("foo") + .withNoParameters(Parameters.class) + .execute(); + //@formatter:on + + assertEquals(1, resp.getParameter().size()); + assertEquals(CustomTypeDstu3Test.MyCustomPatient.class, resp.getParameter().get(0).getResource().getClass()); + assertEquals("http://example.com/fhir/Patient/$foo", capt.getAllValues().get(1).getURI().toASCIIString()); + } + + private List> toTypeList(Class theClass) { + ArrayList> retVal = new ArrayList>(); + retVal.add(theClass); + return retVal; + } + @Test public void testUserAgentForBinary() throws Exception { IParser p = ourCtx.newXmlParser(); diff --git a/src/site/xdoc/doc_extensions.xml b/src/site/xdoc/doc_extensions.xml index 273434c2f78..2199778f7d6 100644 --- a/src/site/xdoc/doc_extensions.xml +++ b/src/site/xdoc/doc_extensions.xml @@ -185,6 +185,29 @@ +

+ You may also explicitly use custom types in searches and other + operations which return resources. +

+ + + + + +

+ You can also explicitly declare a preferred response resource custom + type. This is useful for some operations that do not otherwise + declare their resource types in the method signature. +

+ + + + + + + + +

Sometimes you may not know in advance exactly which type you will be receiving. For example, there are Patient resources @@ -192,6 +215,11 @@ aren't sure which profile you will get back for a specific read, you can declare the "primary" type for a given profile.

+

+ This is declared at the FhirContext level, and will apply to any + clients created from this context (including clients created before + the default was set). +