package edu.gatech.i3l.fhir.dstu2.entities; import java.math.BigDecimal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; //import javax.persistence.criteria.ParameterExpression; import javax.persistence.criteria.Root; import javax.validation.constraints.NotNull; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.api.IDatatype; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.composite.PeriodDt; import ca.uhn.fhir.model.dstu2.composite.QuantityDt; import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu2.composite.SimpleQuantityDt; import ca.uhn.fhir.model.dstu2.resource.Observation.Component; //import ca.uhn.fhir.model.dstu2.resource.Observation.Related; //import ca.uhn.fhir.model.dstu2.valueset.ObservationRelationshipTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.ObservationStatusEnum; import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.StringDt; import edu.gatech.i3l.fhir.jpa.dao.BaseFhirDao; import edu.gatech.i3l.fhir.jpa.entity.BaseResourceEntity; import edu.gatech.i3l.fhir.jpa.entity.IResourceEntity; import edu.gatech.i3l.omop.enums.Omop4ConceptsFixedIds; import edu.gatech.i3l.omop.mapping.OmopConceptMapping; @Entity @Table(name = "f_observation_view") public class Observation extends BaseResourceEntity { private static final String RES_TYPE = "Observation"; private static final ObservationStatusEnum STATUS = ObservationStatusEnum.FINAL; public static final Long SYSTOLIC_CONCEPT_ID = 3004249L; public static final Long DIASTOLIC_CONCEPT_ID = 3012888L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "observation_id") @Access(AccessType.PROPERTY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "person_id", nullable = false) @NotNull private PersonComplement person; @ManyToOne(cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY) @JoinColumn(name = "observation_concept_id", nullable = false) @NotNull private Concept observationConcept; @Column(name = "observation_date", nullable = false) @Temporal(TemporalType.DATE) @NotNull private Date date; @Column(name = "observation_time") // @Temporal(TemporalType.TIME) private String time; @Column(name = "value_as_string") private String valueAsString; @Column(name = "value_as_number") private BigDecimal valueAsNumber; @Column(name = "range_low") private BigDecimal rangeLow; @Column(name = "range_high") private BigDecimal rangeHigh; @ManyToOne(cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY) @JoinColumn(name = "value_as_concept_id") private Concept valueAsConcept; // @ManyToOne(cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY) // @JoinColumn(name = "relevant_condition_concept_id") // private Concept relevantCondition; @ManyToOne(cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY) @JoinColumn(name = "observation_type_concept_id", nullable = false) @NotNull private Concept type; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "provider_id") private Provider provider; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "visit_occurrence_id") private VisitOccurrence visitOccurrence; @Column(name = "source_value") private String sourceValue; @Column(name = "value_source_value") private String valueSourceValue; @ManyToOne(cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY) @JoinColumn(name = "unit_concept_id") private Concept unit; @Column(name = "unit_source_value") private String unitSourceValue; public Observation() { super(); } public Observation(Long id, PersonComplement person, Concept observationConcept, Date date, String time, String valueAsString, BigDecimal valueAsNumber, Concept valueAsConcept, /*Concept relevantCondition,*/ Concept type, Provider provider, VisitOccurrence visitOccurrence, String sourceValue, Concept unit, String unitsSourceValue) { super(); this.id = id; this.person = person; this.observationConcept = observationConcept; this.date = date; this.time = time; this.valueAsString = valueAsString; this.valueAsNumber = valueAsNumber; this.valueAsConcept = valueAsConcept; // this.relevantCondition = relevantCondition; this.type = type; this.provider = provider; this.visitOccurrence = visitOccurrence; this.sourceValue = sourceValue; this.unit = unit; this.unitSourceValue = unitsSourceValue; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } public BigDecimal getRangeLow() { return rangeLow; } public void setRangeLow(BigDecimal rangeLow) { this.rangeLow = rangeLow; } public BigDecimal getRangeHigh() { return rangeHigh; } public void setRangeHigh(BigDecimal rangeHigh) { this.rangeHigh = rangeHigh; } public PersonComplement getPerson() { return person; } public void setPerson(PersonComplement person) { this.person = person; } public Concept getObservationConcept() { return observationConcept; } public void setObservationConcept(Concept observationConcept) { this.observationConcept = observationConcept; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public String getValueAsString() { return valueAsString; } public void setValueAsString(String valueAsString) { this.valueAsString = valueAsString; } public BigDecimal getValueAsNumber() { return valueAsNumber; } public void setValueAsNumber(BigDecimal valueAsNumber) { this.valueAsNumber = valueAsNumber; } public Concept getValueAsConcept() { return valueAsConcept; } public void setValueAsConcept(Concept valueAsConcept) { this.valueAsConcept = valueAsConcept; } // public Concept getRelevantCondition() { // return relevantCondition; // } // // public void setRelevantCondition(Concept relevantCondition) { // this.relevantCondition = relevantCondition; // } public Concept getType() { return type; } public void setType(Concept type) { this.type = type; } public Provider getProvider() { return provider; } public void setProvider(Provider provider) { this.provider = provider; } public VisitOccurrence getVisitOccurrence() { return visitOccurrence; } public void setVisitOccurrence(VisitOccurrence visitOccurrence) { this.visitOccurrence = visitOccurrence; } public String getSourceValue() { return sourceValue; } public void setSourceValue(String sourceValue) { this.sourceValue = sourceValue; } public Concept getUnit() { return unit; } public void setUnit(Concept unit) { this.unit = unit; } public String getUnitSourceValue() { return unitSourceValue; } public void setUnitSourceValue(String unitSourceValue) { this.unitSourceValue = unitSourceValue; } public String getValueSourceValue() { return valueSourceValue; } public void setValueSourceValue(String valueSourceValue) { this.valueSourceValue = valueSourceValue; } @Override public IResourceEntity constructEntityFromResource(IResource resource) { System.out.println("Trying to write to Observation View Table"); // TODO: This is view, which is read-only. We need to come up with a way to write // to either measurement or observation tables in OMOP. We may write them manually // and just return null for this. But then, response will not be correct. Revisit this. ca.uhn.fhir.model.dstu2.resource.Observation observation = (ca.uhn.fhir.model.dstu2.resource.Observation) resource; OmopConceptMapping ocm = OmopConceptMapping.getInstance(); if (observation.getEffective() instanceof DateTimeDt) { this.date = ((DateTimeDt) observation.getEffective()).getValue(); SimpleDateFormat timeFormat = new SimpleDateFormat ("HH:mm:ss"); this.time = timeFormat.format(((DateTimeDt) observation.getEffective()).getValue()); } else if (observation.getEffective() instanceof PeriodDt) { // TODO: we need to handle period. We can probably use // we can use range_low and range_high. These are only available in Measurement } /* * Set subject: currently supporting only type Person TODO create * entity-complement to specify other types of subjects */ IdDt reference = observation.getSubject().getReference(); if (reference.getIdPartAsLong() != null) { if ("Patient".equals(reference.getResourceType())) { this.person = new PersonComplement(); this.person.setId(reference.getIdPartAsLong()); } else if ("Group".equals(reference.getResourceType())) { // } else if ("Device".equals(reference.getResourceType())) { // } else if ("Location".equals(reference.getResourceType())) { // } } /* Set visit occurrence */ Long visitOccurrenceId = observation.getEncounter().getReference().getIdPartAsLong(); if (visitOccurrenceId != null) { this.visitOccurrence = new VisitOccurrence(); this.visitOccurrence.setId(visitOccurrenceId); } Long observationConceptId = ocm.get(observation.getCode().getCodingFirstRep().getCode(), OmopConceptMapping.LOINC_CODE); if (observationConceptId != null) { this.observationConcept = new Concept(); this.observationConcept.setId(observationConceptId); } /* Set the type of the observation */ this.type = new Concept(); if (observation.getMethod().getCodingFirstRep() != null) { this.type.setId(Omop4ConceptsFixedIds.OBSERVATION_FROM_LAB_NUMERIC_RESULT.getConceptId()); // assuming // all // results // on // this // table // are // quantitative: // http://hl7.org/fhir/2015May/valueset-observation-methods.html } else { this.type.setId(Omop4ConceptsFixedIds.OBSERVATION_FROM_EHR.getConceptId()); } /* Set the value of the observation */ IDatatype value = observation.getValue(); if (value instanceof QuantityDt) { Long unitId = ocm.get(((QuantityDt) value).getUnit(), OmopConceptMapping.UCUM_CODE, OmopConceptMapping.UCUM_CODE_STANDARD, OmopConceptMapping.UCUM_CODE_CUSTOM); this.valueAsNumber = ((QuantityDt) value).getValue(); if (unitId != null) { this.unit = new Concept(); this.unit.setId(unitId); } this.rangeHigh = observation.getReferenceRangeFirstRep().getHigh().getValue(); this.rangeLow = observation.getReferenceRangeFirstRep().getLow().getValue(); } else if (value instanceof CodeableConceptDt) { Long valueAsConceptId = ocm.get(((CodeableConceptDt) value).getCodingFirstRep().getCode(), OmopConceptMapping.CLINICAL_FINDING); if (valueAsConceptId != null) { this.valueAsConcept = new Concept(); this.valueAsConcept.setId(valueAsConceptId); } } else { this.valueAsString = ((StringDt) value).getValue(); } // quick solution. this.sourceValue = "NA"; return this; } @Override public FhirVersionEnum getFhirVersion() { return FhirVersionEnum.DSTU2; } @Override public IResource getRelatedResource() { ca.uhn.fhir.model.dstu2.resource.Observation observation = new ca.uhn.fhir.model.dstu2.resource.Observation(); observation.setId(this.getIdDt()); String systemUriString = this.observationConcept.getVocabulary().getSystemUri(); String codeString = this.observationConcept.getConceptCode(); String displayString; if (this.observationConcept.getId() == 0L) { displayString = this.getSourceValue(); } else { displayString = this.observationConcept.getName(); } // OMOP database maintains Systolic and Diastolic Blood Pressures separately. // FHIR however keeps them together. Observation DAO filters out Diastolic values. // Here, when we are reading systolic, we search for matching diastolic and put them // together. The Observation ID will be systolic's OMOP ID. // public static final Long SYSTOLIC_CONCEPT_ID = new Long(3004249); // public static final Long DIASTOLIC_CONCEPT_ID = new Long(3012888); if (SYSTOLIC_CONCEPT_ID.equals(this.observationConcept.getId())) { // Set coding for systolic and diastolic observation systemUriString = "http://loinc.org"; codeString = "55284-4"; displayString = "Blood pressure systolic & diastolic"; List<Component> components = new ArrayList<Component>(); // First we add systolic component. Component comp = new Component(); CodeableConceptDt componentCode = new CodeableConceptDt(this.observationConcept.getVocabulary().getSystemUri(), this.observationConcept.getConceptCode()); componentCode.getCodingFirstRep().setDisplay(this.observationConcept.getName()); comp.setCode(componentCode); IDatatype compValue = null; if (this.valueAsNumber != null) { QuantityDt quantity = new QuantityDt(this.valueAsNumber.doubleValue()); // Unit is defined as a concept code in omop v4, then unit and code are the same in this case if (this.unit != null) { quantity.setUnit(this.unit.getConceptCode()); quantity.setCode(this.unit.getConceptCode()); quantity.setSystem(this.unit.getVocabulary().getSystemUri()); } compValue = quantity; comp.setValue(compValue); components.add(comp); } // Now search for diastolic component. WebApplicationContext myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); EntityManager entityManager = myAppCtx.getBean("myBaseDao", BaseFhirDao.class).getEntityManager(); CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<Observation> criteria = builder.createQuery(Observation.class); Root<Observation> from = criteria.from(Observation.class); criteria.select(from).where( builder.equal(from.get("observationConcept").get("id"), DIASTOLIC_CONCEPT_ID), builder.equal(from.get("person").get("id"), this.person.getId()), builder.equal(from.get("date"), this.date), builder.equal(from.get("time"), this.time) ); TypedQuery<Observation> query = entityManager.createQuery(criteria); List<Observation> results = query.getResultList(); if (results.size() > 0) { Observation diastolicOb = results.get(0); comp = new Component(); componentCode = new CodeableConceptDt(diastolicOb.observationConcept.getVocabulary().getSystemUri(), diastolicOb.observationConcept.getConceptCode()); componentCode.getCodingFirstRep().setDisplay(diastolicOb.observationConcept.getName()); comp.setCode(componentCode); compValue = null; if (diastolicOb.valueAsNumber != null) { QuantityDt quantity = new QuantityDt(diastolicOb.valueAsNumber.doubleValue()); // Unit is defined as a concept code in omop v4, then unit and code are the same in this case if (diastolicOb.unit != null) { quantity.setUnit(diastolicOb.unit.getConceptCode()); quantity.setCode(diastolicOb.unit.getConceptCode()); quantity.setSystem(diastolicOb.unit.getVocabulary().getSystemUri()); } compValue = quantity; comp.setValue(compValue); components.add(comp); } } if (components.size() > 0) { observation.setComponent(components); } } else { IDatatype value = null; if (this.valueAsNumber != null) { QuantityDt quantity = new QuantityDt(this.valueAsNumber.doubleValue()); if (this.unit != null) { // Unit is defined as a concept code in omop v4, then unit and code are the same in this case quantity.setUnit(this.unit.getConceptCode()); quantity.setCode(this.unit.getConceptCode()); quantity.setSystem(this.unit.getVocabulary().getSystemUri()); } value = quantity; } else if (this.valueAsString != null) { value = new StringDt(this.valueAsString); } else if (this.valueAsConcept != null && this.valueAsConcept.getId() != 0L) { // vocabulary is a required attribute for concept, then it's expected to not be null CodeableConceptDt valueAsConcept = new CodeableConceptDt(this.valueAsConcept.getVocabulary().getSystemUri(), this.valueAsConcept.getConceptCode()); value = valueAsConcept; } else { value = new StringDt(this.getValueSourceValue()); } observation.setValue(value); } if (this.rangeLow != null) observation.getReferenceRangeFirstRep().setLow(new SimpleQuantityDt(this.rangeLow.doubleValue())); if (this.rangeHigh != null) observation.getReferenceRangeFirstRep().setHigh(new SimpleQuantityDt(this.rangeHigh.doubleValue())); CodeableConceptDt code = new CodeableConceptDt(systemUriString, codeString); code.getCodingFirstRep().setDisplay(displayString); observation.setCode(code); observation.setStatus(STATUS); if (this.date != null) { SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); String dateString = fmt.format(this.date); fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date myDate = null; try { if (this.time != null && this.time.isEmpty() == false) { myDate = fmt.parse(dateString+" "+this.time); } else { myDate = this.date; } } catch (ParseException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (myDate != null) { DateTimeDt appliesDate = new DateTimeDt(myDate); observation.setEffective(appliesDate); } } // if (// this.date != null && // this.time != null) { // WARNING notice that the resource field // // 'appliesDate' relies only on the entity field // // 'time' // DateTimeDt appliesDate = new DateTimeDt(this.time); // observation.setEffective(appliesDate); // } if (this.person != null) { ResourceReferenceDt personRef = new ResourceReferenceDt(this.person.getIdDt()); personRef.setDisplay(this.person.getNameAsSingleString()); observation.setSubject(personRef); } if (this.visitOccurrence != null) observation.getEncounter().setReference(new IdDt (VisitOccurrence.RES_TYPE, this.visitOccurrence.getId())); if (this.type != null) { if (this.type.getId() == 44818701L) { // This is From physical examination. CodeableConceptDt typeConcept = new CodeableConceptDt(); List<CodingDt> typeCodings = new ArrayList<CodingDt>(); CodingDt typeCoding = new CodingDt("http://hl7.org/fhir/observation-category", "exam"); typeCodings.add(typeCoding); typeConcept.setCoding(typeCodings); observation.setCategory(typeConcept); } else if (this.type.getId() == 44818702L) { CodeableConceptDt typeConcept = new CodeableConceptDt(); // This is Lab result List<CodingDt> typeCodings = new ArrayList<CodingDt>(); CodingDt typeCoding = new CodingDt("http://hl7.org/fhir/observation-category", "laboratory"); typeCodings.add(typeCoding); typeConcept.setCoding(typeCodings); observation.setCategory(typeConcept); } else if (this.type.getId() == 45905771L) { CodeableConceptDt typeConcept = new CodeableConceptDt(); // This is Lab result List<CodingDt> typeCodings = new ArrayList<CodingDt>(); CodingDt typeCoding = new CodingDt("http://hl7.org/fhir/observation-category", "survey"); typeCodings.add(typeCoding); typeConcept.setCoding(typeCodings); observation.setCategory(typeConcept); } else if (this.type.getId() == 38000277L || this.type.getId() == 38000278L) { CodeableConceptDt typeConcept = new CodeableConceptDt(); // This is Lab result List<CodingDt> typeCodings = new ArrayList<CodingDt>(); CodingDt typeCoding = new CodingDt("http://hl7.org/fhir/observation-category", "laboratory"); typeCodings.add(typeCoding); typeConcept.setCoding(typeCodings); observation.setCategory(typeConcept); } else if (this.type.getId() == 38000280L || this.type.getId() == 38000281L) { CodeableConceptDt typeConcept = new CodeableConceptDt(); // This is Lab result List<CodingDt> typeCodings = new ArrayList<CodingDt>(); CodingDt typeCoding = new CodingDt("http://hl7.org/fhir/observation-category", "exam"); typeCodings.add(typeCoding); typeConcept.setCoding(typeCodings); observation.setCategory(typeConcept); } } return observation; } @Override public String getResourceType() { return RES_TYPE; } @Override public InstantDt getUpdated() { // TODO Auto-generated method stub return null; } @Override public String translateSearchParam(String theSearchParam) { System.out.println("Observation Search:"+theSearchParam); switch (theSearchParam) { case ca.uhn.fhir.model.dstu2.resource.Observation.SP_SUBJECT: return "person"; case ca.uhn.fhir.model.dstu2.resource.Observation.SP_PATIENT: return "person"; case ca.uhn.fhir.model.dstu2.resource.Observation.SP_ENCOUNTER: return "visitOccurrence"; case ca.uhn.fhir.model.dstu2.resource.Observation.SP_VALUE_QUANTITY: return "valueAsNumber"; case ca.uhn.fhir.model.dstu2.resource.Observation.SP_VALUE_STRING: return "valueAsString"; case ca.uhn.fhir.model.dstu2.resource.Observation.SP_VALUE_CONCEPT: return "valueAsConcept"; default: break; } return theSearchParam; } }