...
 
Commits (77)
......@@ -37,13 +37,14 @@
<birthdate>2001-01-01</birthdate>
<deathdate>2020</deathdate>
<sex>F</sex>
<encounter start="2014-01-01T10:30:00" end="2014-01-05T10:30:00">XXE12345</encounter>
<encounter start="2014-01-01T10:30:00" end="2014-01-05T10:30:00.123">XXE12345</encounter>
<location>Zuhause</location>
<!-- TODO inpatient/outpatient -->
<provider>xxxa</provider>
<facts>
<!-- test parsing of partial time stamps -->
<eav-item concept="T:date:secs" start="2014-09-07T10:40:03"/>
<eav-item concept="T:date:msec" start="2014-09-07T10:40:03.123"/>
<eav-item concept="T:date:mins" start="2014-09-07T10:40"/>
<eav-item concept="T:date:hours" start="2014-09-07T10"/>
<eav-item concept="T:date:day" start="2014-09-07"/>
......
......@@ -23,9 +23,10 @@ XX12345 XXE12345 gest dat 2020
XX12345 XXE12345 sex str F
XX12345 XXE12345 psurname str Dampf
XX12345 XXE12345 pnames str A B
XX12345 XXE12345 visit @ @ @ 2014-01-01T10:30:00 2014-01-05T10:30:00 xxxa Zuhause
XX12345 XXE12345 visit @ @ @ 2014-01-01T10:30:00 2014-01-05T10:30:00.123 xxxa Zuhause
# normale werte
XX12345 XXE12345 T:date:secs @ @ @ 2014-09-07T10:40:03
XX12345 XXE12345 T:date:msec @ @ @ 2014-09-07T10:40:03.123
XX12345 XXE12345 T:date:mins @ @ @ 2014-09-07T10:40
XX12345 XXE12345 T:date:hours @ @ @ 2014-09-07T10
XX12345 XXE12345 T:date:day @ @ @ 2014-09-07
......
......@@ -4,12 +4,12 @@
<groupId>de.sekmi.histream</groupId>
<artifactId>histream-core</artifactId>
<version>0.14-SNAPSHOT</version>
<version>0.16-SNAPSHOT</version>
<parent>
<groupId>de.sekmi.histream</groupId>
<artifactId>histream</artifactId>
<version>0.14-SNAPSHOT</version>
<version>0.16-SNAPSHOT</version>
</parent>
<build>
......
......@@ -26,9 +26,9 @@ package de.sekmi.histream;
* Extensions allow additional information to be stored and retrieved
* for observations.
*
* @author Raphael
* @author R.W.Majeed
*
* @param <T> type class
* @param <T> slot type to be kept in memory with the observation
*/
public interface Extension<T>{
/**
......@@ -58,6 +58,23 @@ public interface Extension<T>{
* TODO change return type to array, to register all compatible classes
* @return instance type
*/
Iterable<Class<? super T>> getInstanceTypes();
Class<?>[] getInstanceTypes();
Class<T> getSlotType();
/**
* Extract subtype information from the slot type.
* E.g. a visit store can provide info about the patient
* @param slotInstance slot instance type
* @param subtype subtype to retrieve
* @return subtype instance
*/
<U> U extractSubtype(T slotInstance, Class<U> subtype);
public static <U,T> U extractSupertype(T slotInstance, Class<U> supertype){
if( supertype.isInstance(slotInstance) ) {
return supertype.cast(slotInstance);
}else {
throw new IllegalArgumentException("Unsupported supertype "+supertype);
}
}
}
package de.sekmi.histream;
import java.io.IOException;
import java.time.Instant;
import de.sekmi.histream.ext.Visit;
/**
* Extracts observations from complex sources
* such as databases and data warehouses.
......@@ -14,12 +17,14 @@ public interface ObservationExtractor {
/**
* Extract observations with a start time stamp between the specified limits.
* Only observations with the specified notations are extracted.
* TODO evaluate change from ObservationException to IOException
*
* @param start_min minimum time for observation start (inclusive)
* @param start_max maximum time for observation start (inclusive)
* @param notations concept notations. Specifies which observations are extracted.
* @return supplier for the extracted observations. Must be closed after use.
* @throws ObservationException error (e.g. database failure)
* @throws IOException error (e.g. database failure)
*/
ObservationSupplier extract(Instant start_min, Instant start_max, Iterable<String> notations) throws ObservationException;
ObservationSupplier extract(Instant start_min, Instant start_max, Iterable<String> notations) throws IOException;
ObservationSupplier extract(Iterable<Visit> visits, Iterable<String> notations) throws IOException;
}
package de.sekmi.histream.ext;
import java.util.List;
public interface PatientVisitStore {
Patient findPatient(String patientId);
Visit findVisit(String visitId);
void merge(Patient patient, String additionalId, ExternalSourceType source);
/**
* Get alias ids for the given patient (e.g. resulting from a merge)
* @param patient patient instance
* @return alias ids
*/
String[] getPatientAliasIds(Patient patient);
/**
* Deletes the patient identified by given id. This method does not remove any other associated
* data e.g. like visits, observations.
* @param id patient id
*/
void purgePatient(String patientId);
void purgeVisit(String visitId);
List<? extends Visit> allVisits(Patient patient);
}
......@@ -42,6 +42,9 @@ public interface Visit extends IdExtensionType,ExternalSourceType {
String getLocationId();
void setLocationId(String locationId);
String getProviderId();
void setProviderId(String providerId);
public enum Status{
Inpatient, Outpatient, Emergency
}
......
......@@ -9,6 +9,7 @@ import java.util.Map;
* @author R.W.Majeed
*
*/
@Deprecated
public class CachedPatientExtension extends SimplePatientExtension {
private Map<String, PatientImpl> cache;
......
......@@ -82,9 +82,9 @@ public abstract class GroupedObservationHandler implements ObservationHandler, A
Visit thisVisit = observation.getExtension(Visit.class);
// assertations to simplify troubleshooting corrupt data
Objects.requireNonNull(thisPatient);
Objects.requireNonNull(thisPatient.getId());
Objects.requireNonNull(thisPatient.getId(),"Patient w/o ID");
Objects.requireNonNull(thisVisit);
Objects.requireNonNull(thisVisit.getId());
Objects.requireNonNull(thisVisit.getId(),"Visit w/o ID");
if( prevPatient == null ){
// write start document, meta, patient
......
package de.sekmi.histream.impl;
import java.util.Arrays;
/*
* #%L
* histream
......@@ -35,11 +32,12 @@ import de.sekmi.histream.ext.Patient;
* @author R.W.Majeed
*
*/
@Deprecated
public class SimplePatientExtension implements Extension<PatientImpl>{
private final static Iterable<Class<? super PatientImpl>> TYPES = Arrays.asList(Patient.class, PatientImpl.class);
private final static Class<?>[] TYPES = new Class[] {Patient.class, PatientImpl.class};
@Override
public Iterable<Class<? super PatientImpl>> getInstanceTypes() {return TYPES;}
public Class<?>[] getInstanceTypes() {return TYPES;}
@Override
public PatientImpl createInstance(Object... args) {
......@@ -63,4 +61,14 @@ public class SimplePatientExtension implements Extension<PatientImpl>{
return patient;
}
@Override
public Class<PatientImpl> getSlotType() {
return PatientImpl.class;
}
@Override
public <U> U extractSubtype(PatientImpl slotInstance, Class<U> subtype) {
return extractSubtype(slotInstance, subtype);
}
}
package de.sekmi.histream.impl;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import de.sekmi.histream.Observation;
import de.sekmi.histream.Extension;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.Visit;
public class SimplePatientVisitExtension implements Extension<VisitPatientImpl>{
private final static Class<?>[] TYPES = new Class[] {Visit.class,VisitPatientImpl.class,Patient.class,PatientImpl.class};
@Override
public Class<?>[] getInstanceTypes() {return TYPES;}
@Override
public VisitPatientImpl createInstance(Object... args) {
if( args.length != 3
|| !(args[0] instanceof String)
|| !(args[1] instanceof String)
|| !(args[2] instanceof ExternalSourceType) )
{
throw new IllegalArgumentException("Need arguments Patient id, Visit id, ExternalSourceType");
}
ExternalSourceType source = (ExternalSourceType)args[2];
PatientImpl patient = new PatientImpl();
patient.setId((String)args[0]);
patient.setSourceId(source.getSourceId());
patient.setSourceTimestamp(source.getSourceTimestamp());
VisitPatientImpl visit = new VisitPatientImpl((String)args[1], patient, null);
visit.setSourceId(source.getSourceId());
visit.setSourceTimestamp(source.getSourceTimestamp());
return visit;
}
@Override
public VisitPatientImpl createInstance(Observation observation) {
VisitPatientImpl visit = createInstance(observation.getPatientId(), observation.getEncounterId(), observation.getSource());
//visit.setId();
//visit.setPatientId(observation.getPatientId());
//visit.setSourceId(observation.getSourceId());
//visit.setSourceTimestamp(observation.getSourceTimestamp());
return visit;
}
@Override
public Class<VisitPatientImpl> getSlotType() {
return VisitPatientImpl.class;
}
@Override
public <U> U extractSubtype(VisitPatientImpl slotInstance, Class<U> subtype) {
if( subtype.isAssignableFrom(PatientImpl.class) ){
return subtype.cast(slotInstance.getPatient());
}else if( subtype.isInstance(slotInstance) ) {
return subtype.cast(slotInstance);
}else {
throw new IllegalArgumentException("Unsupported subtype "+subtype);
}
}
}
......@@ -23,18 +23,18 @@ package de.sekmi.histream.impl;
import de.sekmi.histream.Observation;
import java.util.Arrays;
import de.sekmi.histream.Extension;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.Visit;
@Deprecated
public class SimpleVisitExtension implements Extension<VisitImpl>{
private final static Iterable<Class<? super VisitImpl>> TYPES = Arrays.asList(Visit.class, VisitImpl.class);
private final static Class<?>[] TYPES = new Class[] {Visit.class, VisitImpl.class};
@Override
public Iterable<Class<? super VisitImpl>> getInstanceTypes() {return TYPES;}
public Class<?>[] getInstanceTypes() {return TYPES;}
@Override
public VisitImpl createInstance(Object... args) {
......@@ -47,7 +47,7 @@ public class SimpleVisitExtension implements Extension<VisitImpl>{
}
VisitImpl visit = new VisitImpl();
visit.setId((String)args[0]);
visit.setPatientId(((Patient)args[1]).getId());
visit.setPatient(((Patient)args[1]));
ExternalSourceType source = (ExternalSourceType)args[2];
visit.setSourceId(source.getSourceId());
visit.setSourceTimestamp(source.getSourceTimestamp());
......@@ -65,4 +65,13 @@ public class SimpleVisitExtension implements Extension<VisitImpl>{
return visit;
}
@Override
public Class<VisitImpl> getSlotType() {
return VisitImpl.class;
}
@Override
public <U> U extractSubtype(VisitImpl slotInstance, Class<U> subtype) {
return extractSubtype(slotInstance, subtype);
}
}
package de.sekmi.histream.impl;
import java.util.Objects;
/*
* #%L
* histream
......@@ -24,38 +26,49 @@ package de.sekmi.histream.impl;
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.StoredExtensionType;
import de.sekmi.histream.ext.Visit;
@Deprecated
public class VisitImpl extends StoredExtensionType implements Visit {
private DateTimeAccuracy startTime;
private DateTimeAccuracy endTime;
private Status status;
private String patientId;
private String locationId;
private String providerId;
public VisitImpl(){
/**
* Empty constructor protected, only
* available to overriding classes.
*/
protected VisitImpl() {
}
public VisitImpl(String id, String patientId, DateTimeAccuracy startTime, DateTimeAccuracy endTime, Status status){
public VisitImpl(String id, String patientId, DateTimeAccuracy startTime){
setId(id);
this.patientId = patientId;
this.startTime = startTime;
markDirty(true);
}
public VisitImpl(String id, String patientId, DateTimeAccuracy startTime, DateTimeAccuracy endTime, Status status){
this(id, patientId, startTime);
this.status = status;
this.startTime = startTime;
this.endTime = endTime;
markDirty(true);
}
public String getPatientId(){return patientId;}
public void setPatientId(String patientId){
public void setPatient(Patient patient){
Objects.requireNonNull(patient);
// patient id should not be changed normally.
this.patientId = patientId;
this.patientId = patient.getId();
// TODO need to update dirty flag?
markDirty(true);
}
@Override
public DateTimeAccuracy getStartTime() {
return startTime;
......@@ -99,6 +112,15 @@ public class VisitImpl extends StoredExtensionType implements Visit {
checkAndUpdateDirty(this.startTime, startTime);
this.startTime = startTime;
}
@Override
public String getProviderId() {
return this.providerId;
}
@Override
public void setProviderId(String providerId) {
checkAndUpdateDirty(this.providerId, providerId);
this.providerId = providerId;
}
......
package de.sekmi.histream.impl;
import java.util.Objects;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.ext.StoredExtensionType;
import de.sekmi.histream.ext.Visit;
public class VisitPatientImpl extends StoredExtensionType implements Visit {
private PatientImpl patient;
private DateTimeAccuracy startTime;
private DateTimeAccuracy endTime;
private Status status;
private String locationId;
private String providerId;
/**
* Empty constructor protected, only
* available to overriding classes.
*/
protected VisitPatientImpl() {
}
public VisitPatientImpl(String id, PatientImpl patient, DateTimeAccuracy startTime){
setId(id);
setPatient(patient);
this.startTime = startTime;
}
public String getPatientId(){return patient.getId();}
public void setPatient(PatientImpl patient){
Objects.requireNonNull(patient);
// patient id should not be changed normally.
this.patient = patient;
markDirty(true);
}
public PatientImpl getPatient() {
return this.patient;
}
@Override
public DateTimeAccuracy getStartTime() {
return startTime;
}
@Override
public DateTimeAccuracy getEndTime() {
return endTime;
}
@Override
public Status getStatus() {
return this.status;
}
@Override
public void setStatus(Status status) {
checkAndUpdateDirty(this.status, status);
this.status = status;
}
@Override
public String getLocationId() {
return locationId;
}
@Override
public void setLocationId(String locationId){
checkAndUpdateDirty(this.locationId, locationId);
this.locationId = locationId;
}
@Override
public void setEndTime(DateTimeAccuracy endTime) {
checkAndUpdateDirty(this.endTime, endTime);
this.endTime = endTime;
}
@Override
public void setStartTime(DateTimeAccuracy startTime) {
checkAndUpdateDirty(this.startTime, startTime);
this.startTime = startTime;
}
@Override
public String getProviderId() {
return this.providerId;
}
@Override
public void setProviderId(String providerId) {
checkAndUpdateDirty(this.providerId, providerId);
this.providerId = providerId;
}
}
......@@ -28,8 +28,6 @@ import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Hashtable;
import java.util.Map;
import java.util.logging.Logger;
......@@ -242,7 +240,7 @@ public class FlatObservationSupplier extends AbstractObservationParser implement
}
private DateTimeAccuracy getSourceDateTime(){
return new DateTimeAccuracy(LocalDateTime.ofInstant(sourceTimestamp, ZoneId.systemDefault()));
return new DateTimeAccuracy(sourceTimestamp);
}
private void lazyCreatePatient(String patientId){
if( currentPatient == null || !currentPatient.getId().equals(patientId) ){
......
......@@ -4,12 +4,14 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.text.ParseException;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
......@@ -29,6 +31,7 @@ import de.sekmi.histream.impl.ExternalSourceImpl;
import de.sekmi.histream.impl.Meta;
import de.sekmi.histream.impl.ObservationFactoryImpl;
import de.sekmi.histream.impl.ObservationImpl;
import de.sekmi.histream.xml.DateTimeAccuracyAdapter;
/**
* Read grouped observations from XML
......@@ -42,6 +45,7 @@ public class GroupedXMLReader implements ObservationSupplier {
static final String PATIENT_ELEMENT = "patient";
static final String ENCOUNTER_ELEMENT = "encounter";
static final String FACT_ELEMENT = "fact";
public static final QName ROOT_ELEMENT = new QName(GroupedXMLWriter.NAMESPACE, DOCUMENT_ROOT);
private ObservationFactory factory;
private ExtensionAccessor<Patient> patientAccessor;
......@@ -61,32 +65,51 @@ public class GroupedXMLReader implements ObservationSupplier {
private DateTimeAccuracy encounterStart;
private DateTimeAccuracy encounterEnd;
private Map<String,String> visitData;
private ZoneId zoneId;
@Deprecated
public GroupedXMLReader(ObservationFactory factory, InputStream input)throws JAXBException, XMLStreamException, FactoryConfigurationError{
this(factory, input, null);
}
/**
* The provided {@code input} is not closed by a call to {@link #close()}
*
* @param factory observation factory
* @param input XML input
* @param localZone zone to use for local timestamps
* @throws JAXBException JAXB error
* @throws XMLStreamException XML stream error
* @throws FactoryConfigurationError other error
*/
public GroupedXMLReader(ObservationFactory factory, InputStream input)throws JAXBException, XMLStreamException, FactoryConfigurationError{
this(factory, XMLInputFactory.newInstance().createXMLStreamReader(input));
public GroupedXMLReader(ObservationFactory factory, InputStream input, ZoneId localZone)throws JAXBException, XMLStreamException, FactoryConfigurationError{
this(factory, XMLInputFactory.newInstance().createXMLStreamReader(input), localZone);
}
@Deprecated // TODO remove method without zoneid
public GroupedXMLReader(ObservationFactory factory, XMLStreamReader reader) throws JAXBException, XMLStreamException{
this(factory,reader,null);
}
/**
* Construct a reader with a {@link XMLStreamReader}. The {@code reader} is closed when {@link #close()} is called.
* @param factory observation factory
* @param reader xml reader
* @param localZone zone to use for local timestamps
* @throws JAXBException jaxb error
* @throws XMLStreamException stream error
*/
public GroupedXMLReader(ObservationFactory factory, XMLStreamReader reader) throws JAXBException, XMLStreamException{
public GroupedXMLReader(ObservationFactory factory, XMLStreamReader reader, ZoneId localZone) throws JAXBException, XMLStreamException{
super();
this.factory = factory;
this.patientData = new HashMap<>();
this.visitData = new HashMap<>();
unmarshaller = JAXBContext.newInstance(ObservationImpl.class,Meta.class).createUnmarshaller();
this.zoneId = localZone;
if( zoneId != null ){
// modify marshaller to use the timezone for timestamps
DateTimeAccuracyAdapter a = new DateTimeAccuracyAdapter();
a.setZoneId(zoneId);
unmarshaller.setAdapter(DateTimeAccuracyAdapter.class, a);
}
// TODO: set schema
//unmarshaller.setSchema(schema);
this.reader = reader;
......@@ -103,7 +126,7 @@ public class GroupedXMLReader implements ObservationSupplier {
readPatient();
readEncounter();
}
private void readToRoot() throws XMLStreamException{
while( reader.hasNext() ){
reader.next();
......@@ -155,7 +178,7 @@ public class GroupedXMLReader implements ObservationSupplier {
if( patientData.containsKey("birthdate") ){
String dob = patientData.get("birthdate");
try {
currentPatient.setBirthDate(DateTimeAccuracy.parsePartialIso8601(dob));
currentPatient.setBirthDate(DateTimeAccuracy.parsePartialIso8601(dob, zoneId));
} catch (ParseException e) {
throw new XMLStreamException("Unable to parse birthdate: "+dob, reader.getLocation(), e);
}
......@@ -167,7 +190,7 @@ public class GroupedXMLReader implements ObservationSupplier {
// will be empty string for <deceased/>
if( date != null && date.length() != 0 ){
try {
currentPatient.setDeathDate(DateTimeAccuracy.parsePartialIso8601(date));
currentPatient.setDeathDate(DateTimeAccuracy.parsePartialIso8601(date, zoneId));
} catch (ParseException e) {
throw new XMLStreamException("Unable to parse deceased date: "+date, reader.getLocation(), e);
}
......@@ -238,7 +261,7 @@ public class GroupedXMLReader implements ObservationSupplier {
if( visitData.containsKey("start") ){
String date = visitData.get("start");
try {
encounterStart = DateTimeAccuracy.parsePartialIso8601(date);
encounterStart = DateTimeAccuracy.parsePartialIso8601(date, zoneId);
} catch (ParseException e) {
throw new XMLStreamException("Unable to parse encounter/start: "+date, reader.getLocation(), e);
}
......@@ -248,7 +271,7 @@ public class GroupedXMLReader implements ObservationSupplier {
if( visitData.containsKey("end") ){
String date = visitData.get("end");
try {
encounterEnd = DateTimeAccuracy.parsePartialIso8601(date);
encounterEnd = DateTimeAccuracy.parsePartialIso8601(date, zoneId);
} catch (ParseException e) {
throw new XMLStreamException("Unable to parse encounter/end: "+date, reader.getLocation(), e);
}
......
package de.sekmi.histream.io;
import java.io.OutputStream;
import java.time.ZoneId;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
......@@ -12,6 +13,8 @@ import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Result;
import org.w3c.dom.DOMException;
import de.sekmi.histream.Observation;
import de.sekmi.histream.ObservationException;
import de.sekmi.histream.ext.ExternalSourceType;
......@@ -21,6 +24,7 @@ import de.sekmi.histream.impl.ExternalSourceImpl;
import de.sekmi.histream.impl.GroupedObservationHandler;
import de.sekmi.histream.impl.Meta;
import de.sekmi.histream.impl.ObservationImpl;
import de.sekmi.histream.xml.DateTimeAccuracyAdapter;
/**
* Writes observations to a single XML file. Observations must be grouped by patient and encounter.
......@@ -36,6 +40,7 @@ public class GroupedXMLWriter extends GroupedObservationHandler{
private int formattingDepth;
private Meta meta;
private int observationCount;
private ZoneId zoneId;
/**
* Used to output XML
......@@ -101,6 +106,18 @@ public class GroupedXMLWriter extends GroupedObservationHandler{
} catch (PropertyException e) {
}
}
/**
* Set the timezone id for output of timestamp values.
* @param timeZone zone id or {@code null} to omit zone offset information
*/
public void setZoneId(ZoneId timeZone){
this.zoneId = timeZone;
// modify marshaller to use the timezone for timestamps
DateTimeAccuracyAdapter a = new DateTimeAccuracyAdapter();
a.setZoneId(zoneId);
marshaller.setAdapter(DateTimeAccuracyAdapter.class, a);
}
@Override
protected void beginStream() throws ObservationException{
......@@ -162,14 +179,14 @@ public class GroupedXMLWriter extends GroupedObservationHandler{
if( visit.getStartTime() != null ){
formatIndent();
writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,"start",NAMESPACE);
writer.writeCharacters(visit.getStartTime().toPartialIso8601());
writer.writeCharacters(visit.getStartTime().toPartialIso8601(zoneId));
writer.writeEndElement();
formatNewline();
}
if( visit.getEndTime() != null ){
formatIndent();
writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,"end",NAMESPACE);
writer.writeCharacters(visit.getEndTime().toPartialIso8601());
writer.writeCharacters(visit.getEndTime().toPartialIso8601(zoneId));
writer.writeEndElement();
formatNewline();
}
......@@ -270,7 +287,7 @@ public class GroupedXMLWriter extends GroupedObservationHandler{
if( patient.getBirthDate() != null ){
formatIndent();
writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,"birthdate",NAMESPACE);
writer.writeCharacters(patient.getBirthDate().toPartialIso8601());
writer.writeCharacters(patient.getBirthDate().toPartialIso8601(zoneId));
writer.writeEndElement();
formatNewline();
}
......@@ -280,7 +297,7 @@ public class GroupedXMLWriter extends GroupedObservationHandler{
formatIndent();
writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,"deceased",NAMESPACE);
if( patient.getDeathDate() != null ){
writer.writeCharacters(patient.getDeathDate().toPartialIso8601());
writer.writeCharacters(patient.getDeathDate().toPartialIso8601(zoneId));
}
writer.writeEndElement();
formatNewline();
......@@ -379,7 +396,7 @@ public class GroupedXMLWriter extends GroupedObservationHandler{
formatIndent();
marshalFactWithContext(observation, observation.getExtension(Visit.class), meta.source);
formatNewline();
} catch (JAXBException | XMLStreamException e) {
} catch (JAXBException | XMLStreamException | DOMException e) {
throw new ObservationException(e);
}
this.observationCount ++;
......
......@@ -36,6 +36,7 @@ import de.sekmi.histream.Observation;
import de.sekmi.histream.ObservationFactory;
import de.sekmi.histream.ObservationSupplier;
// TODO remove this class
@Deprecated
public class XMLObservationSupplier extends XMLObservationParser implements ObservationSupplier{
//private static final String namespaceURI = "http://sekmi.de/histream/dwh-eav";
......
package de.sekmi.histream.xml;
import java.text.ParseException;
import java.time.ZoneId;
import javax.xml.bind.annotation.adapters.XmlAdapter;
......@@ -13,17 +14,31 @@ import de.sekmi.histream.DateTimeAccuracy;
*
*/
public class DateTimeAccuracyAdapter extends XmlAdapter<String, DateTimeAccuracy>{
private ZoneId zoneId;
/**
* Specify a local time zone. If non-{@code null}, the result of {@link #marshal(DateTimeAccuracy)}
* will always include the offset of the specified zone.
* When unmarshalling a string without offset, then the timestamp is treated as if it was
* in the specified zone id.
* @param zoneId local zone id to use as default for both {@link #marshal(DateTimeAccuracy)} and {@link #unmarshal(String)}
*/
public void setZoneId(ZoneId zoneId){
this.zoneId = zoneId;
}
@Override
public DateTimeAccuracy unmarshal(String v) throws ParseException {
if( v == null )return null;
return DateTimeAccuracy.parsePartialIso8601(v);
// parsing will support any zone offset
// if zone is missing, assume specified zone
return DateTimeAccuracy.parsePartialIso8601(v, zoneId);
}
@Override
public String marshal(DateTimeAccuracy v) {
if( v == null )return null;
return v.toPartialIso8601();
return v.toPartialIso8601(zoneId);
}
}
package de.sekmi.histream;
import java.text.ParseException;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.UnsupportedTemporalTypeException;
import org.junit.Assert;
import static org.junit.Assert.*;
import org.junit.Test;
public class TestDateTimeAccuracy {
@Test
public void testParseYYYYDD(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M.u");
String text = "02.2003";
DateTimeAccuracy a = DateTimeAccuracy.parse(formatter, text);
DateTimeAccuracy a = DateTimeAccuracy.parse(formatter, text, ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.MONTHS,a.getAccuracy());
Assert.assertEquals(2, a.get(ChronoField.MONTH_OF_YEAR));
Assert.assertEquals(2003, a.get(ChronoField.YEAR));
TemporalAccessor ac = a.toInstantMin().atOffset(ZoneOffset.UTC);
Assert.assertEquals(2, ac.get(ChronoField.MONTH_OF_YEAR));
Assert.assertEquals(2003, ac.get(ChronoField.YEAR));
}
@Test
public void testParseTimestampWithoutSeparators(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuuMMddHHmm");
String text = "201705190800";
DateTimeAccuracy a = DateTimeAccuracy.parse(formatter, text, ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.MINUTES,a.getAccuracy());
TemporalAccessor ac = a.toInstantMin().atOffset(ZoneOffset.UTC);
Assert.assertEquals(5, ac.get(ChronoField.MONTH_OF_YEAR));
Assert.assertEquals(2017, ac.get(ChronoField.YEAR));
}
@Test
public void verifyZoneOffset(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.M.u[ H[:m[:s]]]");
DateTimeAccuracy a;
ZoneId tz = ZoneId.of("Asia/Shanghai"); // China standard time
a = DateTimeAccuracy.parse(formatter, "01.02.2003", ZoneOffset.UTC);
Assert.assertEquals("2003-02-01", a.toPartialIso8601(null));
Assert.assertEquals("2003-02-01", a.toPartialIso8601(tz));
a = DateTimeAccuracy.parse(formatter, "01.02.2003 13", ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.HOURS, a.getAccuracy());
Assert.assertEquals("2003-02-01T13", a.toPartialIso8601(null));
Assert.assertEquals("2003-02-01T21+0800", a.toPartialIso8601(tz));
}
@Test
public void verifyDateTimeFormatter(){
TemporalAccessor a;
DateTimeFormatter f = DateTimeFormatter.ISO_DATE_TIME;
// zone offset missing, field should be not available
a = f.parse("2001-02-03T04:05:06");
Assert.assertFalse(a.isSupported(ChronoField.OFFSET_SECONDS));
try{
a.get(ChronoField.OFFSET_SECONDS);
Assert.fail("Expected exception not thrown");
}catch( UnsupportedTemporalTypeException e ){
// expected outcome
}
// zero zone offset, field should be available
a = f.parse("2001-02-03T04:05:06Z");
Assert.assertEquals(0, a.get(ChronoField.OFFSET_SECONDS));
a = f.parse("2001-02-03T04:05:06+00:00");
Assert.assertEquals(0, a.get(ChronoField.OFFSET_SECONDS));
a = f.parse("2001-02-03T04:05"); // seconds can be omitted
// test the partial timestamp formatter
f = DateTimeFormatter.ofPattern("u[-M[-d['T'H[:m[:s[.S]]][X]]]]");
}
@Test
public void verifyParsingIncompleteIsoTimestamp() throws ParseException{
DateTimeAccuracy a;
a = DateTimeAccuracy.parsePartialIso8601("2001");
assertEquals(ChronoUnit.YEARS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02");
assertEquals(ChronoUnit.MONTHS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03");
assertEquals(ChronoUnit.DAYS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04");
assertEquals(ChronoUnit.HOURS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05");
assertEquals(ChronoUnit.MINUTES, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06");
assertEquals(ChronoUnit.SECONDS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06.789");
assertEquals(ChronoUnit.MILLIS, a.getAccuracy());
// verify zone offset
// for second accuracy
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06+0800");
assertEquals(ChronoUnit.SECONDS, a.getAccuracy());
// zone offset calculation
assertEquals(DateTimeAccuracy.parsePartialIso8601("2001-02-02T20:05:06Z"), a);
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04+0800");
assertEquals(ChronoUnit.HOURS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04Z");
assertEquals(ChronoUnit.HOURS, a.getAccuracy());
a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05+0800");
assertEquals(ChronoUnit.MINUTES, a.getAccuracy());
}
@Test
public void testFormatExceedsText(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.M.u[ H[:m[:s]]]");
formatter.withResolverStyle(ResolverStyle.STRICT);
//DateTimeFormatterBuilder b = new DateTimeFormatterBuilder();
DateTimeAccuracy a;
a = DateTimeAccuracy.parse(formatter, "01.02.2003");
a = DateTimeAccuracy.parse(formatter, "01.02.2003", ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.DAYS,a.getAccuracy());
a = DateTimeAccuracy.parse(formatter, "01.02.2003 13");
//Assert.assertEquals(ChronoUnit.HOURS,a.getAccuracy());
//will have second resolution (implicit)
// TODO correct accuracy
a = DateTimeAccuracy.parse(formatter, "01.02.2003 13", ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.HOURS,a.getAccuracy());
a = DateTimeAccuracy.parse(formatter, "01.02.2003 13:14", ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.MINUTES,a.getAccuracy());
}
@Test
public void verifyIncompleteIsoDateException() throws ParseException{
try {
DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06+");
Assert.fail();
} catch (ParseException e) {
}
// TODO test more aspects of zone offset parsing
DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06Z");
DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2003-02-01T04:05:06+0100");
// make sure the date is adjusted to UTC
TemporalAccessor ac = a.toInstantMin().atOffset(ZoneOffset.UTC);
Assert.assertEquals(3, ac.get(ChronoField.HOUR_OF_DAY));
}
@Test
public void verifyParseExceptionBehavior(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("u");
// verify expected behavior of DateTimeFormatter
formatter.parse("2003");
try{
formatter.parse("+");
Assert.fail("Exception expected unexpected input");
}catch( DateTimeParseException e ){
}
try{
formatter.parse("2003+");
Assert.fail("Exception expected for unparsed text at end of input");
}catch( DateTimeParseException e ){
}
// verify same behavior for DateTimeAccurecy
// should not fail below
DateTimeAccuracy a = DateTimeAccuracy.parse(formatter, "2003" ,ZoneOffset.UTC);
Assert.assertEquals(ChronoUnit.YEARS, a.getAccuracy());
// next two calls should throw exceptions
try{
DateTimeAccuracy.parse(formatter, "+", ZoneOffset.UTC);
Assert.fail("Exception unexpected input");
}catch( DateTimeParseException e ){
}
try{
DateTimeAccuracy.parse(formatter, "2003+", ZoneOffset.UTC);
Assert.fail("Exception expected for unparsed text at end of input");
}catch( DateTimeParseException e ){
}
}
@Test
public void verifyParsingWithLocalZone() throws ParseException{
DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04", ZoneId.of("Asia/Shanghai"));
// date should be treated as if it had a +08:00 offset
assertEquals("2001-02-02T20Z", a.toPartialIso8601(ZoneId.of("UTC")));
}
@Test
public void verifyMillisecondParseAndToString() throws ParseException{
DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:05:06.789", ZoneId.of("UTC"));
// date should be treated as if it had a +08:00 offset
assertEquals("2001-02-03T04:05:06.789Z", a.toPartialIso8601(ZoneId.of("UTC")));
}
@Test
public void verifyComparison() throws ParseException{
ZoneId zone = ZoneOffset.UTC.normalized();
DateTimeAccuracy a = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04", zone);
DateTimeAccuracy b = DateTimeAccuracy.parsePartialIso8601("2001-02-03T04:00", zone);
// instant min are equal
Assert.assertEquals(a.toInstantMin(), b.toInstantMin());
// yet a and b are not equal
Assert.assertNotEquals(a, b);
// defined order
Assert.assertTrue(a.compareTo(b) > 0);
Assert.assertTrue(b.compareTo(a) < 0);
}
// TODO: further tests
}
......@@ -35,9 +35,10 @@ public class ECMAEvaluatorTest {
}
// skip to string value
for( int i=0; i<6; i++ ){
for( int i=0; i<7; i++ ){
o = s.get();
}
Assert.assertEquals("T:type:str", o.getConceptId());
// compare string value
Assert.assertEquals(true, eval.test("fact.value != null", o));
......
......@@ -8,6 +8,7 @@ import java.io.StringWriter;
import java.math.BigDecimal;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneOffset;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXB;
......@@ -61,7 +62,7 @@ public class ObservationImplJAXBTest {
o.conceptId = "C"+index;
o.patientId = "P"+index;
o.encounterId = "E"+index;
o.startTime = new DateTimeAccuracy(2015,1,1,index);
o.startTime = new DateTimeAccuracy(ZoneOffset.UTC, 2015,1,1,index);
switch( index ){
case 0:
// string value
......
package de.sekmi.histream.impl;
import java.text.ParseException;
import java.time.ZoneOffset;
/*
* #%L
......@@ -36,9 +37,9 @@ public class TestDateTimeAccuracy {
static ChronoField[] fields = {ChronoField.YEAR, ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.MILLI_OF_SECOND};
private void assertFieldValues(Temporal temporal, int[] values){
private void assertFieldValues(DateTimeAccuracy a, int[] values){
for( int i=0; i<values.length; i++ ){
Assert.assertEquals(values[i], temporal.get(fields[i]));
Assert.assertEquals(values[i], a.toInstantMin().atOffset(ZoneOffset.UTC).get(fields[i]));
}
}
@Test
......
......@@ -38,9 +38,10 @@ public class XPathEvaluatorTest {
Assert.assertEquals(expr, true, eval.test(expr, o));
}
// skip to string value
for( int i=0; i<6; i++ ){
for( int i=0; i<7; i++ ){
o = s.get();
}
Assert.assertEquals("T:type:str", o.getConceptId());
// compare string value
Assert.assertEquals(true, eval.test("f:fact/f:value='abc123'", o));
......
......@@ -24,6 +24,7 @@ package de.sekmi.histream.io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.ZoneOffset;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
......@@ -77,7 +78,7 @@ public class FileObservationProviderTest {
(Observation o) -> {
Assert.assertEquals("T:date:secs", o.getConceptId());
Assert.assertEquals(ChronoUnit.SECONDS, o.getStartTime().getAccuracy());
Assert.assertEquals(3, o.getStartTime().getLong(ChronoField.SECOND_OF_MINUTE));
Assert.assertEquals(3, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.SECOND_OF_MINUTE));
Patient p = o.getExtension(Patient.class);
Assert.assertNotNull("Patient extension required", p);
Assert.assertEquals("XX12345", p.getId());
......@@ -100,7 +101,7 @@ public class FileObservationProviderTest {
Assert.assertEquals("Zuhause", v.getLocationId());
try{
Assert.assertEquals(DateTimeAccuracy.parsePartialIso8601("2014-01-01T10:30:00"), v.getStartTime());
Assert.assertEquals(DateTimeAccuracy.parsePartialIso8601("2014-01-05T10:30:00"), v.getEndTime());
Assert.assertEquals(DateTimeAccuracy.parsePartialIso8601("2014-01-05T10:30:00.123"), v.getEndTime());
} catch (ParseException e) {
throw new RuntimeException(e);
}
......@@ -110,30 +111,36 @@ public class FileObservationProviderTest {
ExternalSourceType s = o.getSource();
Assert.assertNotNull(s);
},
(Observation o) -> {
Assert.assertEquals("T:date:msec", o.getConceptId());
// TODO store and calculate time in nanos
Assert.assertEquals(ChronoUnit.MILLIS, o.getStartTime().getAccuracy());
Assert.assertEquals(123, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.MILLI_OF_SECOND));
},
(Observation o) -> {
Assert.assertEquals("T:date:mins", o.getConceptId());
Assert.assertEquals(ChronoUnit.MINUTES, o.getStartTime().getAccuracy());
Assert.assertEquals(40, o.getStartTime().getLong(ChronoField.MINUTE_OF_HOUR));
Assert.assertEquals(40, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.MINUTE_OF_HOUR));
},
(Observation o) -> {
Assert.assertEquals("T:date:hours", o.getConceptId());
Assert.assertEquals(ChronoUnit.HOURS, o.getStartTime().getAccuracy());
Assert.assertEquals(10, o.getStartTime().getLong(ChronoField.HOUR_OF_DAY));
Assert.assertEquals(10, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.HOUR_OF_DAY));
},
(Observation o) -> {
Assert.assertEquals("T:date:day", o.getConceptId());
Assert.assertEquals(ChronoUnit.DAYS, o.getStartTime().getAccuracy());
Assert.assertEquals(7, o.getStartTime().getLong(ChronoField.DAY_OF_MONTH));
Assert.assertEquals(7, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.DAY_OF_MONTH));
},
(Observation o) -> {
Assert.assertEquals("T:date:month", o.getConceptId());
Assert.assertEquals(ChronoUnit.MONTHS, o.getStartTime().getAccuracy());
Assert.assertEquals(9, o.getStartTime().getLong(ChronoField.MONTH_OF_YEAR));
Assert.assertEquals(9, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.MONTH_OF_YEAR));
},
(Observation o) -> {
Assert.assertEquals("T:date:year", o.getConceptId());
Assert.assertEquals(ChronoUnit.YEARS, o.getStartTime().getAccuracy());
Assert.assertEquals(2014, o.getStartTime().getLong(ChronoField.YEAR));
Assert.assertEquals(2014, o.getStartTime().toInstantMin().atOffset(ZoneOffset.UTC).getLong(ChronoField.YEAR));
},
(Observation o) -> {
Assert.assertEquals("T:type:str", o.getConceptId());
......@@ -158,10 +165,10 @@ public