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.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.util.Assert;
import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.model.api.IElement; import ca.uhn.fhir.model.api.IElement;
@ -64,7 +65,10 @@ import ca.uhn.fhir.validation.FhirValidator;
* Important usage notes: * Important usage notes:
* </p> * </p>
* <ul> * <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> * <li>
* Performance: <b>This class is expensive</b> to create, as it scans every resource class it needs to parse or encode * 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 * 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 static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class);
private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM; private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM;
private volatile Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> myClassToElementDefinition = Collections.emptyMap(); 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 Map<String, Class<? extends IBaseResource>> myDefaultTypeForProfile = new HashMap<String, Class<? extends IBaseResource>>();
private volatile Map<String, RuntimeResourceDefinition> myIdToResourceDefinition = Collections.emptyMap(); private volatile Map<String, RuntimeResourceDefinition> myIdToResourceDefinition = Collections.emptyMap();
private HapiLocalizer myLocalizer = new HapiLocalizer(); private HapiLocalizer myLocalizer = new HapiLocalizer();
@ -92,33 +97,55 @@ public class FhirContext {
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition; private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
private final IFhirVersion myVersion; private final IFhirVersion myVersion;
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap(); 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() { public FhirContext() {
this(EMPTY_LIST); 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) { public FhirContext(Class<? extends IBaseResource> theResourceType) {
this(toCollection(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) { public FhirContext(Class<?>... theResourceTypes) {
this(toCollection(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) { public FhirContext(Collection<Class<? extends IBaseResource>> theResourceTypes) {
this(null, 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) { public FhirContext(FhirVersionEnum theVersion) {
this(theVersion, null); this(theVersion, null);
} }
private FhirContext(FhirVersionEnum theVersion, Collection<Class<? extends IBaseResource>> theResourceTypes) { private FhirContext(FhirVersionEnum theVersion, Collection<Class<? extends IBaseResource>> theResourceTypes) {
VersionUtil.getVersion(); VersionUtil.getVersion();
if (theVersion != null) { if (theVersion != null) {
if (!theVersion.isPresentOnClasspath()) { if (!theVersion.isPresentOnClasspath()) {
throw new IllegalStateException(getLocalizer().getMessage(FhirContext.class, "noStructuresForSpecifiedVersion", theVersion.name())); throw new IllegalStateException(getLocalizer().getMessage(FhirContext.class, "noStructuresForSpecifiedVersion", theVersion.name()));
@ -147,6 +174,13 @@ public class FhirContext {
return getLocalizer().getMessage(FhirContext.class, "unknownResourceName", theResourceName, theVersion); 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 * When encoding resources, this setting configures the parser to include
* an entry in the resource's metadata section which indicates which profile(s) the * an entry in the resource's metadata section which indicates which profile(s) the
@ -190,7 +224,7 @@ public class FhirContext {
public BaseRuntimeElementDefinition<?> getElementDefinition(String theElementName) { public BaseRuntimeElementDefinition<?> getElementDefinition(String theElementName) {
return myNameToElementDefinition.get(theElementName.toLowerCase()); return myNameToElementDefinition.get(theElementName.toLowerCase());
} }
/** For unit tests only */ /** For unit tests only */
int getElementDefinitionCount() { int getElementDefinitionCount() {
return myClassToElementDefinition.size(); return myClassToElementDefinition.size();
@ -283,7 +317,7 @@ public class FhirContext {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public RuntimeResourceDefinition getResourceDefinition(String theResourceName) { public RuntimeResourceDefinition getResourceDefinition(String theResourceName) {
Validate.notBlank(theResourceName, "theResourceName must not be blank"); Validate.notBlank(theResourceName, "theResourceName must not be blank");
String resourceName = theResourceName.toLowerCase(); String resourceName = theResourceName.toLowerCase();
RuntimeResourceDefinition retVal = myNameToResourceDefinition.get(resourceName); RuntimeResourceDefinition retVal = myNameToResourceDefinition.get(resourceName);
@ -317,8 +351,9 @@ public class FhirContext {
} }
/** /**
* Get the restful client factory. If no factory has been set, this will be initialized with * Get the restful client factory. If no factory has been set, this will be initialized with
* a new ApacheRestfulClientFactory. * a new ApacheRestfulClientFactory.
*
* @return the factory used to create the restful clients * @return the factory used to create the restful clients
*/ */
public IRestfulClientFactory getRestfulClientFactory() { public IRestfulClientFactory getRestfulClientFactory() {
@ -385,12 +420,12 @@ public class FhirContext {
* </p> * </p>
* *
* @param theClientType * @param theClientType
* The client type, which is an interface type to be instantiated * The client type, which is an interface type to be instantiated
* @param theServerBase * @param theServerBase
* The URL of the base for the restful FHIR server to connect to * The URL of the base for the restful FHIR server to connect to
* @return A newly created client * @return A newly created client
* @throws ConfigurationException * @throws ConfigurationException
* If the interface type is not an interface * If the interface type is not an interface
*/ */
public <T extends IRestfulClient> T newRestfulClient(Class<T> theClientType, String theServerBase) { public <T extends IRestfulClient> T newRestfulClient(Class<T> theClientType, String theServerBase) {
return getRestfulClientFactory().newClient(theClientType, theServerBase); return getRestfulClientFactory().newClient(theClientType, theServerBase);
@ -407,7 +442,7 @@ public class FhirContext {
* </p> * </p>
* *
* @param theServerBase * @param theServerBase
* The URL of the base for the restful FHIR server to connect to * The URL of the base for the restful FHIR server to connect to
*/ */
public IGenericClient newRestfulGenericClient(String theServerBase) { public IGenericClient newRestfulGenericClient(String theServerBase) {
return getRestfulClientFactory().newGenericClient(theServerBase); return getRestfulClientFactory().newGenericClient(theServerBase);
@ -420,7 +455,7 @@ public class FhirContext {
/** /**
* Create a new validator instance. * Create a new validator instance.
* <p> * <p>
* Note on thread safety: Validators are thread safe, you may use a single validator * Note on thread safety: Validators are thread safe, you may use a single validator
* in multiple threads. (This is in contrast to parsers) * in multiple threads. (This is in contrast to parsers)
* </p> * </p>
*/ */
@ -448,6 +483,48 @@ public class FhirContext {
return new XmlParser(this, myParserErrorHandler); 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) { private BaseRuntimeElementDefinition<?> scanDatatype(Class<? extends IElement> theResourceType) {
ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>(); ArrayList<Class<? extends IElement>> resourceTypes = new ArrayList<Class<? extends IElement>>();
resourceTypes.add(theResourceType); resourceTypes.add(theResourceType);
@ -463,7 +540,17 @@ public class FhirContext {
} }
private Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> scanResourceTypes(Collection<Class<? extends IElement>> theResourceTypes) { 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) { if (myRuntimeChildUndeclaredExtensionDefinition == null) {
myRuntimeChildUndeclaredExtensionDefinition = scanner.getRuntimeChildUndeclaredExtensionDefinition(); myRuntimeChildUndeclaredExtensionDefinition = scanner.getRuntimeChildUndeclaredExtensionDefinition();
} }
@ -475,7 +562,7 @@ public class FhirContext {
Map<String, RuntimeResourceDefinition> nameToResourceDefinition = new HashMap<String, RuntimeResourceDefinition>(); Map<String, RuntimeResourceDefinition> nameToResourceDefinition = new HashMap<String, RuntimeResourceDefinition>();
nameToResourceDefinition.putAll(myNameToResourceDefinition); nameToResourceDefinition.putAll(myNameToResourceDefinition);
nameToResourceDefinition.putAll(scanner.getNameToResourceDefinition()); nameToResourceDefinition.putAll(scanner.getNameToResourceDefinition());
Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> classToElementDefinition = new HashMap<Class<? extends IBase>, BaseRuntimeElementDefinition<?>>(); Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> classToElementDefinition = new HashMap<Class<? extends IBase>, BaseRuntimeElementDefinition<?>>();
classToElementDefinition.putAll(myClassToElementDefinition); classToElementDefinition.putAll(myClassToElementDefinition);
classToElementDefinition.putAll(scanner.getClassToElementDefinitions()); classToElementDefinition.putAll(scanner.getClassToElementDefinitions());
@ -502,7 +589,7 @@ public class FhirContext {
return classToElementDefinition; return classToElementDefinition;
} }
/** /**
* When encoding resources, this setting configures the parser to include * When encoding resources, this setting configures the parser to include
* an entry in the resource's metadata section which indicates which profile(s) the * an entry in the resource's metadata section which indicates which profile(s) the
@ -510,7 +597,7 @@ public class FhirContext {
* <p> * <p>
* This feature is intended for situations where custom resource types are being used, * This feature is intended for situations where custom resource types are being used,
* avoiding the need to manually add profile declarations for these custom types. * avoiding the need to manually add profile declarations for these custom types.
* </p> * </p>
* <p> * <p>
* See <a href="http://jamesagnew.gihhub.io/hapi-fhir/doc_extensions.html">Profiling and Extensions</a> * See <a href="http://jamesagnew.gihhub.io/hapi-fhir/doc_extensions.html">Profiling and Extensions</a>
* for more information on using custom types. * for more information on using custom types.
@ -530,7 +617,7 @@ public class FhirContext {
/** /**
* Sets the default type which will be used when parsing a resource that is found to be * Sets the default type which will be used when parsing a resource that is found to be
* of the given profile. * of the given profile.
* <p> * <p>
* For example, this method is invoked with the profile string of * For example, this method is invoked with the profile string of
* <code>"http://example.com/some_patient_profile"</code> and the type of <code>MyPatient.class</code>, * <code>"http://example.com/some_patient_profile"</code> and the type of <code>MyPatient.class</code>,
@ -538,8 +625,11 @@ public class FhirContext {
* the <code>MyPatient</code> type will be used unless otherwise specified. * the <code>MyPatient</code> type will be used unless otherwise specified.
* </p> * </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 theProfile
* @param theClass The resource type. Must not be <code>null</code> or empty. * 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) { public void setDefaultTypeForProfile(String theProfile, Class<? extends IBaseResource> theClass) {
Validate.notBlank(theProfile, "theProfile must not be null or empty"); 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 * 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) { public void setParserErrorHandler(IParserErrorHandler theParserErrorHandler) {
Validate.notNull(theParserErrorHandler, "theParserErrorHandler must not be null"); Validate.notNull(theParserErrorHandler, "theParserErrorHandler must not be null");
@ -571,6 +662,7 @@ public class FhirContext {
/** /**
* Set the restful client factory * Set the restful client factory
*
* @param theRestfulClientFactory * @param theRestfulClientFactory
*/ */
public void setRestfulClientFactory(IRestfulClientFactory 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() { public static FhirContext forDstu2Hl7Org() {
return new FhirContext(FhirVersionEnum.DSTU2_HL7ORG); return new FhirContext(FhirVersionEnum.DSTU2_HL7ORG);

View File

@ -102,7 +102,7 @@ class ModelScanner {
private Set<Class<? extends IBase>> myVersionTypes; private Set<Class<? extends IBase>> myVersionTypes;
ModelScanner(FhirContext theContext, FhirVersionEnum theVersion, Map<Class<? extends IBase>, BaseRuntimeElementDefinition<?>> theExistingDefinitions, 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; myContext = theContext;
myVersion = theVersion; myVersion = theVersion;
Set<Class<? extends IBase>> toScan; 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 * 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) { if (order == Child.ORDER_UNKNOWN) {
order = Integer.MIN_VALUE; order = Integer.valueOf(0);
while (orderMap.containsKey(order)) { while (orderMap.containsKey(order)) {
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.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt; 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.rest.server.AddProfileTagEnum;
import ca.uhn.fhir.util.ElementUtil; import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
@ -42,6 +43,60 @@ public class CustomTypeDstu2Test {
TestUtil.clearAllStaticFieldsForUnitTest(); 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 @Before
public void 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 java.util.List;
import org.hl7.fhir.dstu3.model.Bundle; 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.Medication;
import org.hl7.fhir.dstu3.model.MedicationOrder; import org.hl7.fhir.dstu3.model.MedicationOrder;
import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Quantity; import org.hl7.fhir.dstu3.model.Quantity;
import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.instance.model.api.IBase;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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.api.annotation.ResourceDef;
import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt; 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.rest.server.AddProfileTagEnum;
import ca.uhn.fhir.util.ElementUtil; import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.TestUtil; 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 @Test
public void parseBundleWithResourceDirective() { 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 (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! an HTTP 500. Thanks to Jim Steel for reporting!
</action> </action>
<action type="fix" issue="364">
Enable parsers to parse and serialize custom resources that contain custom datatypes
</action>
</release> </release>
<release version="1.5" date="2016-04-20"> <release version="1.5" date="2016-04-20">
<action type="fix" issue="339"> <action type="fix" issue="339">