Commit 502f8ee6 authored by R.W.Majeed's avatar R.W.Majeed
Browse files

XML serialisation of observations via JAXB

parent d6e4783e
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://sekmi.de/histream/ns/dwh">
<xs:include schemaLocation="fact.xsd"/>
</xs:schema>
\ No newline at end of file
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
><!-- targetNamespace="http://sekmi.de/histream/ns/dwh" -->
<!-- value -->
<xs:complexType name="valueType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="unit" />
<xs:attribute type="xs:string" name="flag" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="numeric">
<xs:simpleContent>
<xs:extension base="valueType">
<xs:attribute type="xs:string" name="operator" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="string">
<xs:simpleContent>
<xs:extension base="valueType"/>
</xs:simpleContent>
</xs:complexType>
<!-- modifier -->
<xs:complexType name="modifierType">
<xs:sequence>
<xs:element name="value" type="valueType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="code" use="required"/>
</xs:complexType>
<xs:simpleType name="partialDate">
<xs:restriction base="xs:string">
<xs:pattern value="[0-9]{4}(-[0-1][0-9](-[0-3][0-9](T[0-2][0-9](:[0-5][0-9](:[0-5][0-9])?)?)?)?)?"></xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="sourceType">
<xs:attribute type="xs:dateTime" name="timestamp" />
<xs:attribute type="xs:string" name="id" />
</xs:complexType>
<xs:complexType name="factType">
<xs:sequence>
<xs:element name="source" type="sourceType" minOccurs="0" maxOccurs="1"/>
<xs:element name="value" type="valueType" minOccurs="0" maxOccurs="1"/>
<xs:element name="modifier" type="modifierType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute type="xs:string" name="patient" />
<xs:attribute type="xs:string" name="encounter" />
<xs:attribute type="xs:string" name="concept" use="required"/>
<xs:attribute type="partialDate" name="start" />
<xs:attribute type="partialDate" name="end"/>
<xs:attribute type="xs:string" name="location" />
<xs:attribute type="xs:string" name="provider" />
</xs:complexType>
<xs:element name="fact" type="factType" />
</xs:schema>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<fact xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="fact.xsd"
patient="12345" encounter="23456" concept="T:testconcept1" start="2015-01-09T22:30" end="2015-01" location="L:ocation" provider="provider">
<value xsi:type="numeric" unit="mm" flag="A" operator="E">123</value>
<modifier code="M:test"><value xsi:type="string">123</value></modifier>
</fact>
\ No newline at end of file
......@@ -29,12 +29,17 @@ import java.time.temporal.TemporalField;
import java.time.temporal.TemporalUnit;
import java.util.Date;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import de.sekmi.histream.xml.DateTimeAccuracyAdapter;
/**
* Local date and time with specified accuracy. Maximum resolution is seconds.
* For supported accuracy, see {@link #setAccuracy(ChronoUnit)}.
* @author Raphael
*
*/
@XmlJavaTypeAdapter(DateTimeAccuracyAdapter.class)
public class DateTimeAccuracy implements Temporal {
private LocalDateTime dateTime;
private ChronoUnit accuracy;
......
......@@ -23,7 +23,6 @@ package de.sekmi.histream;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlEnum;
/**
* Interface for observation value.
......@@ -66,7 +65,6 @@ public interface Value {
* @author marap1
*
*/
@XmlEnum
public enum Type{
/**
* No value (getValue will return null)
......@@ -91,7 +89,6 @@ public interface Value {
* @author marap1
*
*/
@XmlEnum
public enum Operator{
Equal("EQ"),
LessThan("L"),
......
......@@ -23,25 +23,42 @@ package de.sekmi.histream.impl;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import de.sekmi.histream.AbnormalFlag;
import de.sekmi.histream.Value;
@XmlTransient
public abstract class AbstractValue implements Value{
protected AbnormalFlag abnormalFlag;
@XmlAttribute(name="flag")
protected AbnormalFlag flag;
@XmlAttribute(name="unit")
protected String units;
public static AbstractValue NONE = new NilValue();
@Override
public AbnormalFlag getAbnormalFlag() {return abnormalFlag;}
public AbnormalFlag getAbnormalFlag() {return flag;}
public void setAbnormalFlag(AbnormalFlag flag){
this.abnormalFlag = flag;
this.flag = flag;
}
@Override
public String getUnits() {return units;}
/**
* Compare whether the abstract properties defined in this class match
* @param o other abstract value to compare
* @return true when all abstract properties match, false otherwise
*/
protected boolean equals(AbstractValue o){
if( !(o.units == null && this.units == null) && !(this.units != null && this.units.equals(o.units)) )return false;
if( !(o.flag == null && this.flag == null) && !(this.flag != null && this.flag.equals(o.flag)) )return false;
return true;
}
private static class NilValue extends AbstractValue{
@Override
......
......@@ -21,13 +21,30 @@ package de.sekmi.histream.impl;
*/
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import de.sekmi.histream.Modifier;
import de.sekmi.histream.Value;
@XmlAccessorType(XmlAccessType.NONE)
@XmlSeeAlso({StringValue.class,NumericValue.class})
public class ModifierImpl implements Modifier {
@XmlAttribute(name="code")
private String modifierId;
@XmlTransient
private Value value;
/**
* Constructor for JAXB
*/
protected ModifierImpl(){
}
public ModifierImpl(String modifierId){
this.modifierId = modifierId;
}
......@@ -46,4 +63,16 @@ public class ModifierImpl implements Modifier {
this.value = value;
}
/**
* Getter for JAXB
* @return abstract value
*/
@XmlElement(name="value")
protected AbstractValue getAbstractValue(){
return (AbstractValue)value;
}
protected void setAbstractValue(AbstractValue value){
this.value = value;
}
}
......@@ -23,14 +23,41 @@ package de.sekmi.histream.impl;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;
@XmlRootElement(name="value")
@XmlAccessorType(XmlAccessType.NONE)
@XmlType(name="numeric")
public class NumericValue extends AbstractValue {
@XmlValue
private BigDecimal value;
@XmlAttribute
private Operator operator;
private BigDecimal referenceLow;
private BigDecimal referenceHigh;
@Override
public boolean equals(Object other){
if( !other.getClass().equals(NumericValue.class) )return false;
else if( other == this )return true;
NumericValue o = (NumericValue)other;
// compare abstract components
if( !this.equals((AbstractValue)o) )return false;
// compare numeric features
if( !this.operator.equals(o.operator) )return false;
if( !o.value.equals(this.value) )return false;
return true;
}
protected NumericValue(){
this.value = BigDecimal.ZERO;
}
public NumericValue(BigDecimal value){
this(value, null);
}
......
......@@ -22,8 +22,18 @@ package de.sekmi.histream.impl;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.Modifier;
......@@ -37,32 +47,53 @@ import de.sekmi.histream.Value;
* @author Raphael
*
*/
@XmlRootElement(name="fact")
@XmlAccessorType(XmlAccessType.NONE)
@XmlSeeAlso({StringValue.class,NumericValue.class})
public class ObservationImpl implements Observation{
@XmlTransient
protected ObservationFactoryImpl factory;
@XmlAttribute(name="patient")
protected String patientId;
@XmlAttribute(name="encounter")
protected String encounterId;
@XmlAttribute(name="provider")
protected String providerId;
@XmlAttribute(name="location")
protected String locationId;
@XmlAttribute(name="concept")
protected String conceptId;
@XmlTransient
protected Value value;
@XmlAttribute(name="start")
protected DateTimeAccuracy startTime;
@XmlAttribute(name="end")
protected DateTimeAccuracy endTime;
protected Instant sourceTimestamp;
protected String sourceId;
/**
* Modifiers
*/
@XmlTransient // see getModifierList / setModifierList
protected Hashtable<String, ModifierImpl> modifiers;
/**
* Array of extensions, managed by the ObservationFactory
*/
@XmlTransient
protected Object[] extensions;
/**
* Constructor should not be called directly. Instead, use
* {@link ObservationFactory#createObservation(String, String, DateTimeAccuracy)}.
......@@ -71,14 +102,20 @@ public class ObservationImpl implements Observation{
protected ObservationImpl(ObservationFactoryImpl factory){
this.factory = factory;
}
/**
* Constructor for JAXB. Make sure to set the factory after unmarshalling.
*/
protected ObservationImpl(){
}
@Override
public String getPatientId() {return patientId;}
public void setPatientId(String patientId){this.patientId = patientId;}
@Override
public String getEncounterId() {return encounterId;}
@Override
public void setEncounterId(String encounterId){this.encounterId = encounterId;}
......@@ -187,11 +224,49 @@ public class ObservationImpl implements Observation{
return m;
}
/**
* Return the number of modifiers
* @return number of modifiers
*/
public int getModifierCount(){
if( modifiers == null )return 0;
else return modifiers.size();
}
@Override
public void setLocationId(String locationId) {
this.locationId = locationId;
}
/**
* Getter for JAXB
* @return modifier list
*/
@XmlElement(name="modifier")
protected List<ModifierImpl> getModifierList(){
if( modifiers == null || modifiers.isEmpty() )return null;
return new ArrayList<ModifierImpl>(modifiers.values());
}
protected void setModifierList(List<ModifierImpl> list){
modifiers = new Hashtable<String, ModifierImpl>();
for( ModifierImpl i : list ){
modifiers.put(i.getConceptId(), i);
}
}
/**
* Getter for JAXB
* @return abstract value
*/
@XmlElement(name="value")
protected AbstractValue getAbstractValue(){
if( value == null )return null;
else return (AbstractValue)value;
}
protected void setAbstractValue(AbstractValue value){
this.value = value;
}
}
......@@ -23,13 +23,35 @@ package de.sekmi.histream.impl;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;
@XmlRootElement(name="value")
@XmlAccessorType(XmlAccessType.NONE)
@XmlType(name="string")
public class StringValue extends AbstractValue{
protected static final String EMPTY_STRING = "";
@XmlValue
private String value;
public StringValue(){
this.value = EMPTY_STRING;
}
public StringValue(String value){
this.value = value;
}
@Override
public boolean equals(Object other){
if( !StringValue.class.equals(other.getClass()) )return false;
if( other == this )return true;
if( !equals((AbstractValue)other) )return false;
return value.equals(((StringValue)other).value);
}
@Override
public String getValue() {return value;}
......
/**
* Core implementation
*
* @author marap1
*
*/
@XmlSchema(
xmlns = {
@XmlNs(prefix = "xsi", namespaceURI = "http://www.w3.org/2001/XMLSchema-instance")
}
)
package de.sekmi.histream.impl;
import javax.xml.bind.annotation.*;
\ No newline at end of file
package de.sekmi.histream.xml;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import de.sekmi.histream.DateTimeAccuracy;
/**
* Serialize {@link DateTimeAccuracy} to/from a partial ISO8601 string
*
* @author marap1
*
*/
public class DateTimeAccuracyAdapter extends XmlAdapter<String, DateTimeAccuracy>{
@Override
public DateTimeAccuracy unmarshal(String v) {
if( v == null )return null;
return DateTimeAccuracy.parsePartialIso8601(v);
}
@Override
public String marshal(DateTimeAccuracy v) {
if( v == null )return null;
return v.toPartialIso8601();
}
}
/**
* Serialization from/to XML
*
* @author marap1
*
*/
package de.sekmi.histream.xml;
\ No newline at end of file
package de.sekmi.histream.impl;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigDecimal;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXB;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.xml.sax.SAXException;
import de.sekmi.histream.DateTimeAccuracy;
public class ObservationImplJAXBTest {
public static final File EXAMPLE_FACT_XSD = new File("examples/fact.xsd");
public static final File[] EXAMPLE_FACT_FILES = new File[]{
new File("examples/fact1.xml")
};
JAXBContext jaxb;
ObservationFactoryImpl of;
@Before
public void initialize() throws JAXBException{
jaxb = JAXBContext.newInstance(ObservationImpl.class);
of = new ObservationFactoryImpl();
}
private static final int NUM_OBSERVATIONS = 4;
// string observation
private ObservationImpl createObservation(int index){
ObservationImpl o = new ObservationImpl(of);
o.conceptId = "C"+index;
o.patientId = "P"+index;
o.startTime = new DateTimeAccuracy(2015,1,1,index);
switch( index ){
case 0:
// string value
o.setValue(new StringValue("strval"));
break;
case 1:
// numeric value without modifiers
o.setValue(new NumericValue(BigDecimal.TEN));
break;
case 2:
o.setValue(new NumericValue(BigDecimal.TEN));
case 3:
// no value with modifiers
o.addModifier("M:1", new NumericValue(BigDecimal.ONE, "mm"));
o.addModifier("M:2", new StringValue("lalala"));
o.addModifier("M:3", null);
break;
default:
throw new IllegalArgumentException("No test observation with number "+index);
}
return o;
}
@Test
public void testMarshal() throws JAXBException{
for( int i=0; i<NUM_OBSERVATIONS; i++ ){
DOMResult dom = new DOMResult();
ObservationImpl o = createObservation(i);
JAXB.marshal(o, dom);
}
}
@Test
public void testMarshalUnmarshal() throws Exception{
Marshaller m = jaxb.createMarshaller();
Unmarshaller u = jaxb.createUnmarshaller();
for( int i=0; i<NUM_OBSERVATIONS; i++ ){
ObservationImpl o1 = createObservation(i);