Fix #364 - Allow serializing custom types that contain custom datatypes

This commit is contained in:
jamesagnew 2016-05-21 14:35:10 -04:00
parent 021025ffa9
commit 20b6994cc8
7 changed files with 425 additions and 23 deletions

View File

@ -32,6 +32,7 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.util.Assert;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.model.api.IElement;
@ -64,7 +65,10 @@ import ca.uhn.fhir.validation.FhirValidator;
* Important usage notes:
* </p>
* <ul>
* <li>Thread safety: <b>This class is thread safe</b> and may be shared between multiple processing threads.</li>
* <li>
* Thread safety: <b>This class is thread safe</b> and may be shared between multiple processing
* threads, except for the {@link #registerCustomType} and {@link #registerCustomTypes} methods.
* </li>
* <li>
* Performance: <b>This class is expensive</b> to create, as it scans every resource class it needs to parse or encode
* to build up an internal model of those classes. For that reason, you should try to create one FhirContext instance
@ -80,6 +84,7 @@ public class FhirContext {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class);
private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM;
private volatile Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinition = Collections.emptyMap();
private ArrayList<Class<? extends IBase>> myCustomTypes;
private Map<String, Class<? extends IBaseResource>> myDefaultTypeForProfile = new HashMap<String, Class<? extends IBaseResource>>();
private volatile Map<String, RuntimeResourceDefinition> myIdToResourceDefinition = Collections.emptyMap();
private HapiLocalizer myLocalizer = new HapiLocalizer();
@ -94,24 +99,46 @@ public class FhirContext {
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
/**
* Default constructor. In most cases this is the right constructor to use.
* @deprecated It is recommended that you use one of the static initializer methods instead
* of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()}
*/
@Deprecated
public FhirContext() {
this(EMPTY_LIST);
}
/**
* @deprecated It is recommended that you use one of the static initializer methods instead
* of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()}
*/
@Deprecated
public FhirContext(Class<? extends IBaseResource> theResourceType) {
this(toCollection(theResourceType));
}
/**
* @deprecated It is recommended that you use one of the static initializer methods instead
* of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()}
*/
@Deprecated
public FhirContext(Class<?>... theResourceTypes) {
this(toCollection(theResourceTypes));
}
/**
* @deprecated It is recommended that you use one of the static initializer methods instead
* of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()}
*/
@Deprecated
public FhirContext(Collection<Class<? extends IBaseResource>> theResourceTypes) {
this(null, theResourceTypes);
}
/**
* In most cases it is recommended that you use one of the static initializer methods instead
* of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()}, but
* this method can also be used if you wish to supply the version programmatically.
*/
public FhirContext(FhirVersionEnum theVersion) {
this(theVersion, null);
}
@ -147,6 +174,13 @@ public class FhirContext {
return getLocalizer().getMessage(FhirContext.class, "unknownResourceName", theResourceName, theVersion);
}
private void ensureCustomTypeList() {
myClassToElementDefinition.clear();
if (myCustomTypes == null) {
myCustomTypes = new ArrayList<Class<? extends IBase>>();
}
}
/**
* When encoding resources, this setting configures the parser to include
* an entry in the resource's metadata section which indicates which profile(s) the
@ -319,6 +353,7 @@ public class FhirContext {
/**
* Get the restful client factory. If no factory has been set, this will be initialized with
* a new ApacheRestfulClientFactory.
*
* @return the factory used to create the restful clients
*/
public IRestfulClientFactory getRestfulClientFactory() {
@ -448,6 +483,48 @@ public class FhirContext {
return new XmlParser(this, myParserErrorHandler);
}
/**
* This method may be used to register a custom resource or datatype. Note that by using
* custom types, you are creating a system that will not interoperate with other systems that
* do not know about your custom type. There are valid reasons however for wanting to create
* custom types and this method can be used to enable them.
* <p>
* <b>THREAD SAFETY WARNING:</b> This method is not thread safe. It should be called before any
* threads are able to call any methods on this context.
* </p>
*
* @param theType
* The custom type to add (must not be <code>null</code>)
*/
public void registerCustomType(Class<? extends IBase> theType) {
Assert.notNull(theType, "theType must not be null");
ensureCustomTypeList();
myCustomTypes.add(theType);
}
/**
* This method may be used to register a custom resource or datatype. Note that by using
* custom types, you are creating a system that will not interoperate with other systems that
* do not know about your custom type. There are valid reasons however for wanting to create
* custom types and this method can be used to enable them.
* <p>
* <b>THREAD SAFETY WARNING:</b> This method is not thread safe. It should be called before any
* threads are able to call any methods on this context.
* </p>
*
* @param theTypes
* The custom types to add (must not be <code>null</code> or contain null elements in the collection)
*/
public void registerCustomTypes(Collection<Class<? extends IBase>> theTypes) {
Assert.notNull(theTypes, "theTypes must not be null");
Assert.noNullElements(theTypes.toArray(), "theTypes must not contain any null elements");
ensureCustomTypeList();
myCustomTypes.addAll(theTypes);
}
private BaseRuntimeElementDefinition<?> scanDatatype(Class<? extends IElement> theResourceType) {
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>();
resourceTypes.add(theResourceType);
@ -463,7 +540,17 @@ public class FhirContext {
}
private Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> scanResourceTypes(Collection<Class<? extends IElement>> theResourceTypes) {
ModelScanner scanner = new ModelScanner(this, myVersion.getVersion(), myClassToElementDefinition, theResourceTypes);
List<Class<? extends IBase>> typesToScan = new ArrayList<Class<? extends IBase>>();
if (theResourceTypes != null) {
typesToScan.addAll(theResourceTypes);
}
if (myCustomTypes != null) {
typesToScan.addAll(myCustomTypes);
myCustomTypes = null;
}
ModelScanner scanner = new ModelScanner(this, myVersion.getVersion(), myClassToElementDefinition, typesToScan);
if (myRuntimeChildUndeclaredExtensionDefinition == null) {
myRuntimeChildUndeclaredExtensionDefinition = scanner.getRuntimeChildUndeclaredExtensionDefinition();
}
@ -538,8 +625,11 @@ public class FhirContext {
* the <code>MyPatient</code> type will be used unless otherwise specified.
* </p>
*
* @param theProfile The profile string, e.g. <code>"http://example.com/some_patient_profile"</code>. Must not be <code>null</code> or empty.
* @param theClass The resource type. Must not be <code>null</code> or empty.
* @param theProfile
* The profile string, e.g. <code>"http://example.com/some_patient_profile"</code>. Must not be
* <code>null</code> or empty.
* @param theClass
* The resource type. Must not be <code>null</code> or empty.
*/
public void setDefaultTypeForProfile(String theProfile, Class<? extends IBaseResource> theClass) {
Validate.notBlank(theProfile, "theProfile must not be null or empty");
@ -562,7 +652,8 @@ public class FhirContext {
/**
* Sets a parser error handler to use by default on all parsers
*
* @param theParserErrorHandler The error handler
* @param theParserErrorHandler
* The error handler
*/
public void setParserErrorHandler(IParserErrorHandler theParserErrorHandler) {
Validate.notNull(theParserErrorHandler, "theParserErrorHandler must not be null");
@ -571,6 +662,7 @@ public class FhirContext {
/**
* Set the restful client factory
*
* @param theRestfulClientFactory
*/
public void setRestfulClientFactory(IRestfulClientFactory theRestfulClientFactory) {
@ -605,7 +697,8 @@ public class FhirContext {
}
/**
* Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2_HL7ORG DSTU2} (using the Reference Implementation Structures)
* Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2_HL7ORG DSTU2} (using the Reference
* Implementation Structures)
*/
public static FhirContext forDstu2Hl7Org() {
return new FhirContext(FhirVersionEnum.DSTU2_HL7ORG);

View File

@ -102,7 +102,7 @@ class ModelScanner {
private Set<Class<? extends IBase>> myVersionTypes;
ModelScanner(FhirContext theContext, FhirVersionEnum theVersion, Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theExistingDefinitions,
Collection<Class<? extends IElement>> theResourceTypes) throws ConfigurationException {
Collection<Class<? extends IBase>> theResourceTypes) throws ConfigurationException {
myContext = theContext;
myVersion = theVersion;
Set<Class<? extends IBase>> toScan;
@ -467,7 +467,7 @@ class ModelScanner {
* Anything that's marked as unknown is given a new ID that is <0 so that it doesn't conflict with any given IDs and can be figured out later
*/
if (order == Child.ORDER_UNKNOWN) {
order = Integer.MIN_VALUE;
order = Integer.valueOf(0);
while (orderMap.containsKey(order)) {
order++;
}

View File

@ -0,0 +1,92 @@
package ca.uhn.fhir.parser;
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.BaseIdentifiableElement;
import ca.uhn.fhir.model.api.ICompositeDatatype;
import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.api.IResourceBlock;
import ca.uhn.fhir.model.api.annotation.Block;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.model.dstu2.resource.BaseResource;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.util.ElementUtil;
/**
* See #364
*/
@ResourceDef(name = "CustomResource", profile = "http://hl7.org/fhir/profiles/custom-resource", id = "custom-resource")
public class CustomResource364Dstu2 extends BaseResource implements IBaseOperationOutcome {
private static final long serialVersionUID = 1L;
@Child(name = "baseValue", min = 1, max = Child.MAX_UNLIMITED, type= {})
private IElement baseValues;
@Override
public <T extends IElement> List<T> getAllPopulatedChildElementsOfType(Class<T> theType) {
return ElementUtil.allPopulatedChildElements(theType, baseValues);
}
public IElement getBaseValues() {
return baseValues;
}
@Override
public String getResourceName() {
return "CustomResource";
}
@Override
public FhirVersionEnum getStructureFhirVersionEnum() {
return FhirVersionEnum.DSTU2;
}
@Override
public boolean isEmpty() {
return ElementUtil.isEmpty(baseValues);
}
public void setBaseValues(IElement theValue) {
this.baseValues = theValue;
}
@DatatypeDef(name="CustomDate")
public static class CustomResource364CustomDate extends BaseIdentifiableElement implements ICompositeDatatype {
private static final long serialVersionUID = 1L;
@Child(name = "date", order = 0, min = 1, max = 1, type = { DateTimeDt.class })
private DateTimeDt date;
@Override
public <T extends IElement> List<T> getAllPopulatedChildElementsOfType(Class<T> theType) {
return ElementUtil.allPopulatedChildElements(theType, date);
}
public DateTimeDt getDate() {
if (date == null)
date = new DateTimeDt();
return date;
}
@Override
public boolean isEmpty() {
return ElementUtil.isEmpty(date);
}
public CustomResource364CustomDate setDate(DateTimeDt theValue) {
date = theValue;
return this;
}
}
}

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.model.dstu2.resource.MedicationOrder;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.CustomResource364Dstu2.CustomResource364CustomDate;
import ca.uhn.fhir.rest.server.AddProfileTagEnum;
import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.TestUtil;
@ -42,6 +43,60 @@ public class CustomTypeDstu2Test {
TestUtil.clearAllStaticFieldsForUnitTest();
}
/**
* See #364
*/
@Test
public void testCustomTypeWithCustomDatatype() {
FhirContext context = FhirContext.forDstu2();
context.registerCustomType(CustomResource364Dstu2.class);
context.registerCustomType(CustomResource364CustomDate.class);
IParser parser = context.newXmlParser();
CustomResource364Dstu2 resource = new CustomResource364Dstu2();
resource.setBaseValues(new CustomResource364CustomDate().setDate(new DateTimeDt("2016-05-13")));
String xml = parser.encodeResourceToString(resource);
ourLog.info(xml);
//@formatter:on
assertThat(xml, stringContainsInOrder(
"<CustomResource xmlns=\"http://hl7.org/fhir\">",
"<meta><profile value=\"http://hl7.org/fhir/profiles/custom-resource\"/></meta>",
"<baseValueCustomDate><date value=\"2016-05-13\"/></baseValueCustomDate>",
"</CustomResource>"
));
//@formatter:on
CustomResource364Dstu2 parsedResource = parser.parseResource(CustomResource364Dstu2.class, xml);
assertEquals("2016-05-13", ((CustomResource364CustomDate)parsedResource.getBaseValues()).getDate().getValueAsString());
}
/**
* See #364
*/
@Test
public void testCustomTypeWithPrimitiveType() {
FhirContext context = FhirContext.forDstu2();
IParser parser = context.newXmlParser();
CustomResource364Dstu2 resource = new CustomResource364Dstu2();
resource.setBaseValues(new StringDt("2016-05-13"));
String xml = parser.encodeResourceToString(resource);
//@formatter:on
assertThat(xml, stringContainsInOrder(
"<CustomResource xmlns=\"http://hl7.org/fhir\">",
"<meta><profile value=\"http://hl7.org/fhir/profiles/custom-resource\"/></meta>",
"<baseValueString value=\"2016-05-13\"/>",
"</CustomResource>"
));
//@formatter:on
CustomResource364Dstu2 parsedResource = parser.parseResource(CustomResource364Dstu2.class, xml);
assertEquals("2016-05-13", ((StringDt)parsedResource.getBaseValues()).getValueAsString());
}
@Before
public void before() {

View File

@ -0,0 +1,99 @@
package ca.uhn.fhir.parser;
import org.hl7.fhir.dstu3.model.DateTimeType;
import org.hl7.fhir.dstu3.model.DomainResource;
import org.hl7.fhir.dstu3.model.ResourceType;
import org.hl7.fhir.dstu3.model.Type;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.ICompositeType;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.util.ElementUtil;
/**
* This is an example of a custom resource that also uses a custom
* datatype.
*
* See #364
*/
@ResourceDef(name = "CustomResource", profile = "http://hl7.org/fhir/profiles/custom-resource", id = "custom-resource")
public class CustomResource364Dstu3 extends DomainResource {
private static final long serialVersionUID = 1L;
@Child(name = "baseValue", min = 1, max = Child.MAX_UNLIMITED, type= {})
private Type baseValues;
public Type getBaseValues() {
return baseValues;
}
@Override
public FhirVersionEnum getStructureFhirVersionEnum() {
return FhirVersionEnum.DSTU3;
}
@Override
public boolean isEmpty() {
return ElementUtil.isEmpty(baseValues);
}
public void setBaseValues(Type theValue) {
this.baseValues = theValue;
}
@DatatypeDef(name="CustomDate")
public static class CustomResource364CustomDate extends Type implements ICompositeType {
private static final long serialVersionUID = 1L;
@Child(name = "date", order = 0, min = 1, max = 1, type = { DateTimeDt.class })
private DateTimeType date;
public DateTimeType getDate() {
if (date == null)
date = new DateTimeType();
return date;
}
@Override
public boolean isEmpty() {
return ElementUtil.isEmpty(date);
}
public CustomResource364CustomDate setDate(DateTimeType theValue) {
date = theValue;
return this;
}
@Override
protected CustomResource364CustomDate typedCopy() {
CustomResource364CustomDate retVal = new CustomResource364CustomDate();
super.copyValues(retVal);
retVal.date = date;
return retVal;
}
}
@Override
public CustomResource364Dstu3 copy() {
CustomResource364Dstu3 retVal = new CustomResource364Dstu3();
super.copyValues(retVal);
retVal.baseValues = baseValues;
return retVal;
}
@Override
public ResourceType getResourceType() {
return null;
}
}

View File

@ -11,11 +11,13 @@ import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.DateTimeType;
import org.hl7.fhir.dstu3.model.Medication;
import org.hl7.fhir.dstu3.model.MedicationOrder;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Quantity;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.instance.model.api.IBase;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
@ -27,6 +29,7 @@ import ca.uhn.fhir.model.api.annotation.Extension;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.CustomResource364Dstu3.CustomResource364CustomDate;
import ca.uhn.fhir.rest.server.AddProfileTagEnum;
import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.TestUtil;
@ -47,6 +50,63 @@ public class CustomTypeDstu3Test {
}
/**
* See #364
*/
@Test
public void testCustomTypeWithCustomDatatype() {
FhirContext context = FhirContext.forDstu3();
context.registerCustomType(CustomResource364Dstu3.class);
context.registerCustomType(CustomResource364CustomDate.class);
IParser parser = context.newXmlParser();
CustomResource364Dstu3 resource = new CustomResource364Dstu3();
resource.setBaseValues(new CustomResource364CustomDate().setDate(new DateTimeType("2016-05-13")));
String xml = parser.encodeResourceToString(resource);
ourLog.info(xml);
//@formatter:on
assertThat(xml, stringContainsInOrder(
"<CustomResource xmlns=\"http://hl7.org/fhir\">",
"<meta><profile value=\"http://hl7.org/fhir/profiles/custom-resource\"/></meta>",
"<baseValueCustomDate><date value=\"2016-05-13\"/></baseValueCustomDate>",
"</CustomResource>"
));
//@formatter:on
CustomResource364Dstu3 parsedResource = parser.parseResource(CustomResource364Dstu3.class, xml);
assertEquals("2016-05-13", ((CustomResource364CustomDate)parsedResource.getBaseValues()).getDate().getValueAsString());
}
/**
* See #364
*/
@Test
public void testCustomTypeWithPrimitiveType() {
FhirContext context = FhirContext.forDstu3();
context.registerCustomTypes(new ArrayList<Class<? extends IBase>>());
IParser parser = context.newXmlParser();
CustomResource364Dstu3 resource = new CustomResource364Dstu3();
resource.setBaseValues(new StringType("2016-05-13"));
String xml = parser.encodeResourceToString(resource);
//@formatter:on
assertThat(xml, stringContainsInOrder(
"<CustomResource xmlns=\"http://hl7.org/fhir\">",
"<meta><profile value=\"http://hl7.org/fhir/profiles/custom-resource\"/></meta>",
"<baseValueString value=\"2016-05-13\"/>",
"</CustomResource>"
));
//@formatter:on
CustomResource364Dstu3 parsedResource = parser.parseResource(CustomResource364Dstu3.class, xml);
assertEquals("2016-05-13", ((StringType)parsedResource.getBaseValues()).getValueAsString());
}
@Test
public void parseBundleWithResourceDirective() {

View File

@ -208,6 +208,9 @@
(e.g. Patient with an active value of "1") the server should return an HTTP 400, not
an HTTP 500. Thanks to Jim Steel for reporting!
</action>
<action type="fix" issue="364">
Enable parsers to parse and serialize custom resources that contain custom datatypes
</action>
</release>
<release version="1.5" date="2016-04-20">
<action type="fix" issue="339">