Properly deserialize bound codes

This commit is contained in:
James Agnew 2016-03-12 13:23:55 -05:00
parent 232afee955
commit ce253bed70
9 changed files with 292 additions and 172 deletions

View File

@ -1,5 +1,9 @@
package ca.uhn.fhir.model.primitive;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/*
* #%L
* HAPI FHIR - Core Library
@ -42,7 +46,7 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
}
public BoundCodeDt(IValueSetEnumBinder<T> theBinder, T theValue) {
Validate.notNull(theBinder, "theBinder must not be null");
myBinder = theBinder;
@ -52,7 +56,7 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
public IValueSetEnumBinder<T> getBinder() {
return myBinder;
}
public T getValueAsEnum() {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeDt() should not be called!");
T retVal = myBinder.fromCodeString(getValue());
@ -62,6 +66,13 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
return retVal;
}
@SuppressWarnings("unchecked")
@Override
public void readExternal(ObjectInput theIn) throws IOException, ClassNotFoundException {
super.readExternal(theIn);
myBinder = (IValueSetEnumBinder<T>) theIn.readObject();
}
public void setValueAsEnum(T theValue) {
Validate.notNull(myBinder, "This object does not have a binder. Constructor BoundCodeDt() should not be called!");
if (theValue==null) {
@ -70,4 +81,10 @@ public class BoundCodeDt<T extends Enum<?>> extends CodeDt {
setValue(myBinder.toCodeString(theValue));
}
}
@Override
public void writeExternal(ObjectOutput theOut) throws IOException {
super.writeExternal(theOut);
theOut.writeObject(myBinder);
}
}

View File

@ -1,5 +1,7 @@
package org.hl7.fhir.instance.model.api;
import java.io.Serializable;
/*
* #%L
* HAPI FHIR - Core Library
@ -20,7 +22,7 @@ package org.hl7.fhir.instance.model.api;
* #L%
*/
public interface IBaseEnumFactory<T extends Enum<?>> {
public interface IBaseEnumFactory<T extends Enum<?>> extends Serializable {
/**
* Read an enumeration value from the string that represents it on the XML or JSON

View File

@ -1,56 +0,0 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.ServerValidationModeEnum;
public class Tmp {
public static void main(String[] args) {
FhirContext ctx = FhirContext.forDstu2();
ctx.getRestfulClientFactory().setSocketTimeout(200000);
ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/hapi-fhir-jpaserver-example/baseDstu2");
Bundle b = new Bundle();
b.setType(BundleTypeEnum.TRANSACTION);
int resCount = 20;
for (int i = 0; i < (resCount / 2); i++) {
Organization org = new Organization();
org.setId(IdDt.newRandomUuid());
org.setName("Random Org " + i);
org.addAddress().addLine("Random Org Line 1");
org.addIdentifier().setSystem("urn:foo").setValue("some_system" + i);
b.addEntry().setResource(org).getRequest().setMethod(HTTPVerbEnum.POST).setUrl("Organization");
Patient patient = new Patient();
patient.setId(IdDt.newRandomUuid());
patient.addName().addFamily("Family" + i).addGiven("Gigven " + i);
patient.addAddress().addLine("Random Patient Line 1");
patient.addIdentifier().setSystem("urn:bar").setValue("some_system" + i);
b.addEntry().setResource(patient).getRequest().setMethod(HTTPVerbEnum.POST).setUrl("Patient");
}
int total = 0;
long start = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
client.transaction().withBundle(b).execute();
ourLog.info("" + i);
total += resCount;
}
long delay = System.currentTimeMillis() - start;
ourLog.info("Wrote {} resources at {}ms / res", total, delay / total);
//sync 13:57:14.683 [main] INFO ca.uhn.fhir.jpa.config.Tmp - Wrote 6000 resources at 7ms / res
}
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(Tmp.class);
}

View File

@ -31,8 +31,6 @@ import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.model.api.IBoundCodeableConcept;
import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
@DatatypeDef(name = "CodeableConcept", isSpecialization = true)
public class BoundCodeableConceptDt<T extends Enum<?>> extends CodeableConceptDt implements IBoundCodeableConcept {

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.model.dstu2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.Serializable;
import java.util.Date;
import org.apache.commons.io.IOUtils;
@ -15,18 +14,71 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.dstu2.composite.AddressDt;
import ca.uhn.fhir.model.dstu2.composite.HumanNameDt;
import ca.uhn.fhir.model.dstu2.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.AdministrativeGenderEnum;
import ca.uhn.fhir.model.dstu2.valueset.IdentifierTypeCodesEnum;
import ca.uhn.fhir.model.dstu2.valueset.MaritalStatusCodesEnum;
import ca.uhn.fhir.parser.IParser;
public class ModelSerializationTest {
public class ModelSerializationDstu2Test {
private static final FhirContext ourCtx = FhirContext.forDstu2();
/**
* Verify that MaritalStatusCodeEnum (and, by extension, BoundCodeableConcepts in general) are serializable. Author: Nick Peterson (nrpeterson@gmail.com)
*/
@Test
public void testBoundCodeableConceptSerialization() {
MaritalStatusCodesEnum maritalStatus = MaritalStatusCodesEnum.M;
byte[] bytes = SerializationUtils.serialize(maritalStatus);
assertTrue(bytes.length > 0);
MaritalStatusCodesEnum deserialized = SerializationUtils.deserialize(bytes);
assertEquals(maritalStatus.getCode(), deserialized.getCode());
assertEquals(maritalStatus.getSystem(), deserialized.getSystem());
}
@Test
public void testBoundCodeSerialization() {
Patient p = new Patient();
p.setGender(AdministrativeGenderEnum.MALE);
IdentifierDt identifier = p.addIdentifier();
identifier.setType(IdentifierTypeCodesEnum.DL);
Patient out = testIsSerializable(p);
/*
* Make sure the binder still works for Code
*/
assertEquals(AdministrativeGenderEnum.MALE, out.getGenderElement().getValueAsEnum());
out.getGenderElement().setValue("female");
assertEquals(AdministrativeGenderEnum.FEMALE, out.getGenderElement().getValueAsEnum());
assertEquals(IdentifierTypeCodesEnum.DL, out.getIdentifier().get(0).getType().getValueAsEnum().iterator().next());
out.getIdentifier().get(0).getType().setValueAsEnum(IdentifierTypeCodesEnum.MR);
assertEquals("MR", out.getIdentifier().get(0).getType().getCoding().get(0).getCode());
assertEquals("http://hl7.org/fhir/v2/0203", out.getIdentifier().get(0).getType().getCoding().get(0).getSystem());
}
@SuppressWarnings("unchecked")
private <T extends IBaseResource> T testIsSerializable(T theObject) {
byte[] bytes = SerializationUtils.serialize(theObject);
assertTrue(bytes.length > 0);
IBaseResource obj = SerializationUtils.deserialize(bytes);
assertTrue(obj != null);
IParser p = ourCtx.newXmlParser().setPrettyPrint(true);
assertEquals(p.encodeResourceToString(theObject), p.encodeResourceToString(obj));
return (T) obj;
}
@Test
public void testSerialization() throws Exception {
String input = IOUtils.toString(ModelSerializationTest.class.getResourceAsStream("/diagnosticreport-examples-lab-text(72ac8493-52ac-41bd-8d5d-7258c289b5ea).xml"));
String input = IOUtils.toString(ModelSerializationDstu2Test.class.getResourceAsStream("/diagnosticreport-examples-lab-text(72ac8493-52ac-41bd-8d5d-7258c289b5ea).xml"));
Bundle parsed = ourCtx.newXmlParser().parseResource(Bundle.class, input);
testIsSerializable(parsed);
@ -44,31 +96,4 @@ public class ModelSerializationTest {
testIsSerializable(patient);
}
private void testIsSerializable(IBaseResource theObject) {
byte[] bytes = SerializationUtils.serialize(theObject);
assertTrue(bytes.length > 0);
IBaseResource obj = SerializationUtils.deserialize(bytes);
assertTrue(obj != null);
IParser p = ourCtx.newXmlParser().setPrettyPrint(true);
assertEquals(p.encodeResourceToString(theObject), p.encodeResourceToString(obj));
}
/**
* Verify that MaritalStatusCodeEnum (and, by extension, BoundCodeableConcepts in general) are serializable.
* Author: Nick Peterson (nrpeterson@gmail.com)
*/
@Test
public void testBoundCodeableConceptSerialization() {
MaritalStatusCodesEnum maritalStatus = MaritalStatusCodesEnum.M;
byte[] bytes = SerializationUtils.serialize(maritalStatus);
assertTrue(bytes.length > 0);
MaritalStatusCodesEnum deserialized = SerializationUtils.deserialize(bytes);
assertEquals(maritalStatus.getCode(), deserialized.getCode());
assertEquals(maritalStatus.getSystem(), deserialized.getSystem());
}
}

View File

@ -1,5 +1,10 @@
package org.hl7.fhir.dstu3.model;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.hl7.fhir.instance.model.api.IBaseEnumeration;
import ca.uhn.fhir.model.api.annotation.DatatypeDef;
@ -38,11 +43,20 @@ POSSIBILITY OF SUCH DAMAGE.
*
*/
@DatatypeDef(name = "code", isSpecialization = true)
public class Enumeration<T extends Enum<?>> extends PrimitiveType<T> implements IBaseEnumeration<T> {
public class Enumeration<T extends Enum<?>> extends PrimitiveType<T> implements IBaseEnumeration<T>, Externalizable {
private static final long serialVersionUID = 1L;
private final EnumFactory<T> myEnumFactory;
private EnumFactory<T> myEnumFactory;
/**
* Constructor
* @deprecated This no-arg constructor is provided for serialization only - Do not use
*/
@Deprecated
public Enumeration() {
// nothing
}
/**
* Constructor
*/
@ -100,4 +114,18 @@ public class Enumeration<T extends Enum<?>> extends PrimitiveType<T> implements
}
return null;
}
@SuppressWarnings("unchecked")
@Override
public void readExternal(ObjectInput theIn) throws IOException, ClassNotFoundException {
myEnumFactory = (EnumFactory<T>) theIn.readObject();
super.readExternal(theIn);
}
@Override
public void writeExternal(ObjectOutput theOut) throws IOException {
theOut.writeObject(myEnumFactory);
super.writeExternal(theOut);
}
}

View File

@ -1,5 +1,10 @@
package org.hl7.fhir.dstu3.model;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -8,64 +13,18 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.model.api.IElement;
public abstract class PrimitiveType<T> extends Type implements IPrimitiveType<T>, IBaseHasExtensions, IElement {
public abstract class PrimitiveType<T> extends Type implements IPrimitiveType<T>, IBaseHasExtensions, IElement, Externalizable {
private static final long serialVersionUID = 3L;
private T myCoercedValue;
private String myStringValue;
public T getValue() {
return myCoercedValue;
}
public String asStringValue() {
return myStringValue;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(getValue()).toHashCode();
}
public PrimitiveType<T> setValue(T theValue) {
myCoercedValue = theValue;
updateStringValue();
return this;
}
protected void updateStringValue() {
if (myCoercedValue == null) {
myStringValue = null;
} else {
// NB this might be null
myStringValue = encode(myCoercedValue);
}
}
@Override
public boolean isEmpty() {
return super.isEmpty() && StringUtils.isBlank(getValueAsString());
}
public void fromStringValue(String theValue) {
if (theValue == null) {
myCoercedValue = null;
} else {
// NB this might be null
myCoercedValue = parse(theValue);
}
myStringValue = theValue;
}
/**
* Subclasses must override to convert an encoded representation of this datatype into a "coerced" one
*
* @param theValue
* Will not be null
* @return May return null if the value does not correspond to anything
*/
protected abstract T parse(String theValue);
public abstract Type copy();
/**
* Subclasses must override to convert a "coerced" value into an encoded one.
@ -76,37 +35,6 @@ public abstract class PrimitiveType<T> extends Type implements IPrimitiveType<T>
*/
protected abstract String encode(T theValue);
public boolean isPrimitive() {
return true;
}
public String primitiveValue() {
return asStringValue();
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + asStringValue() + "]";
}
public boolean hasValue() {
return !StringUtils.isBlank(getValueAsString());
}
public String getValueAsString() {
return asStringValue();
}
public void setValueAsString(String theValue) {
fromStringValue(theValue);
}
protected Type typedCopy() {
return copy();
}
public abstract Type copy();
@Override
public boolean equalsDeep(Base obj) {
if (!super.equalsDeep(obj))
@ -141,4 +69,92 @@ public abstract class PrimitiveType<T> extends Type implements IPrimitiveType<T>
return b.isEquals();
}
public void fromStringValue(String theValue) {
if (theValue == null) {
myCoercedValue = null;
} else {
// NB this might be null
myCoercedValue = parse(theValue);
}
myStringValue = theValue;
}
public T getValue() {
return myCoercedValue;
}
public String getValueAsString() {
return asStringValue();
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(getValue()).toHashCode();
}
public boolean hasValue() {
return !StringUtils.isBlank(getValueAsString());
}
@Override
public boolean isEmpty() {
return super.isEmpty() && StringUtils.isBlank(getValueAsString());
}
public boolean isPrimitive() {
return true;
}
/**
* Subclasses must override to convert an encoded representation of this datatype into a "coerced" one
*
* @param theValue
* Will not be null
* @return May return null if the value does not correspond to anything
*/
protected abstract T parse(String theValue);
public String primitiveValue() {
return asStringValue();
}
@Override
public void readExternal(ObjectInput theIn) throws IOException, ClassNotFoundException {
String object = (String) theIn.readObject();
setValueAsString(object);
}
public PrimitiveType<T> setValue(T theValue) {
myCoercedValue = theValue;
updateStringValue();
return this;
}
public void setValueAsString(String theValue) {
fromStringValue(theValue);
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + asStringValue() + "]";
}
protected Type typedCopy() {
return copy();
}
protected void updateStringValue() {
if (myCoercedValue == null) {
myStringValue = null;
} else {
// NB this might be null
myStringValue = encode(myCoercedValue);
}
}
@Override
public void writeExternal(ObjectOutput theOut) throws IOException {
theOut.writeObject(getValueAsString());
}
}

View File

@ -0,0 +1,82 @@
package ca.uhn.fhir.model;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Date;
import org.apache.commons.lang3.SerializationUtils;
import org.hl7.fhir.dstu3.model.Address;
import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.dstu3.model.HumanName;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.parser.IParser;
public class ModelSerializationDstu3Test {
private static final FhirContext ourCtx = FhirContext.forDstu3();
/**
* Verify that MaritalStatusCodeEnum (and, by extension, BoundCodeableConcepts in general) are serializable. Author: Nick Peterson (nrpeterson@gmail.com)
*/
@Test
public void testBoundCodeableConceptSerialization() {
AdministrativeGender maritalStatus = AdministrativeGender.MALE;
byte[] bytes = SerializationUtils.serialize(maritalStatus);
assertTrue(bytes.length > 0);
AdministrativeGender deserialized = SerializationUtils.deserialize(bytes);
assertEquals(AdministrativeGender.MALE, deserialized);
}
@Test
public void testBoundCodeSerialization() {
Patient p = new Patient();
p.setGender(AdministrativeGender.MALE);
Patient out = testIsSerializable(p);
/*
* Make sure the binder still works for Code
*/
assertEquals(AdministrativeGender.MALE, out.getGender());
out.getGenderElement().setValueAsString("female");
assertEquals(AdministrativeGender.FEMALE, out.getGender());
}
@SuppressWarnings("unchecked")
private <T extends IBaseResource> T testIsSerializable(T theObject) {
byte[] bytes = SerializationUtils.serialize(theObject);
assertTrue(bytes.length > 0);
IBaseResource obj = SerializationUtils.deserialize(bytes);
assertTrue(obj != null);
IParser p = ourCtx.newXmlParser().setPrettyPrint(true);
assertEquals(p.encodeResourceToString(theObject), p.encodeResourceToString(obj));
return (T) obj;
}
/**
* Contributed by Travis from iSalus
*/
@Test
public void testSerialization2() {
Patient patient = new Patient();
patient.addName(new HumanName().addGiven("George").addFamily("Washington"));
patient.addName(new HumanName().addGiven("George2").addFamily("Washington2"));
patient.addAddress(new Address().addLine("line 1").addLine("line 2").setCity("city").setState("UT"));
patient.addAddress(new Address().addLine("line 1b").addLine("line 2b").setCity("cityb").setState("UT"));
patient.setBirthDate(new Date());
testIsSerializable(patient);
}
}

View File

@ -216,6 +216,14 @@
requests where the URL does not contain an ID but needs to (e.g. for
an update) or contains an ID but shouldn't (e.g. for a create)
</action>
<action type="fix">
When fields of type BoundCodeDt (e.g. Patient.gender)
are serialized and deserialized using Java's native
object serialization, the enum binder was not
serialized too. This meant that values for the
field in the deserialized object could not be
modified. Thanks to Thomas Andersen for reporting!
</action>
</release>
<release version="1.4" date="2016-02-04">
<action type="add">