/** * This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. * * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS * graphic logo is a trademark of OpenMRS Inc. */ package org.openmrs; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.openmrs.annotation.AllowDirectAccess; import org.openmrs.api.APIException; import org.openmrs.api.context.Context; import org.openmrs.obs.ComplexData; import org.openmrs.obs.ComplexObsHandler; import org.openmrs.util.Format; import org.openmrs.util.Format.FORMAT_TYPE; import org.openmrs.util.OpenmrsUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An observation is a single unit of clinical information. <br> * <br> * Observations are collected and grouped together into one Encounter (one visit). Obs can be * grouped in a hierarchical fashion. <br> * <br> * <p> * The {@link #getObsGroup()} method returns an optional parent. That parent object is also an Obs. * The parent Obs object knows about its child objects through the {@link #getGroupMembers()} * method. * </p> * <p> * (Multi-level hierarchies are achieved by an Obs parent object being a member of another Obs * (grand)parent object) Read up on the obs table: http://openmrs.org/wiki/Obs_Table_Primer In an * OpenMRS installation, there may be an occasion need to change an Obs. * </p> * <p> * For example, a site may decide to replace a concept in the dictionary with a more specific set of * concepts. An observation is part of the official record of an encounter. There may be legal, * ethical, and auditing consequences from altering a record. It is recommended that you create a * new Obs and void the old one: * </p> * Obs newObs = Obs.newInstance(oldObs); //copies values from oldObs * newObs.setPreviousVersion(oldObs); * Context.getObsService().saveObs(newObs,"Your reason for the change here"); * Context.getObsService().voidObs(oldObs, "Your reason for the change here"); * * @see Encounter */ public class Obs extends BaseOpenmrsData { /** * @since 2.1.0 */ public enum Interpretation { NORMAL, ABNORMAL, CRITICALLY_ABNORMAL, NEGATIVE, POSITIVE, CRITICALLY_LOW, LOW, HIGH, CRITICALLY_HIGH, VERY_SUSCEPTIBLE, SUSCEPTIBLE, INTERMEDIATE, RESISTANT, SIGNIFICANT_CHANGE_DOWN, SIGNIFICANT_CHANGE_UP, OFF_SCALE_LOW, OFF_SCALE_HIGH } /** * @since 2.1.0 */ public enum Status { PRELIMINARY, FINAL, AMENDED } private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; private static final String TIME_PATTERN = "HH:mm"; private static final String DATE_PATTERN = "yyyy-MM-dd"; public static final long serialVersionUID = 112342333L; private static final Logger log = LoggerFactory.getLogger(Obs.class); private static final String FORM_NAMESPACE_PATH_SEPARATOR = "^"; private static final int FORM_NAMESPACE_PATH_MAX_LENGTH = 255; protected Integer obsId; protected Concept concept; protected Date obsDatetime; protected String accessionNumber; /** * The "parent" of this obs. It is the grouping that brings other obs together. note: * obsGroup.getConcept().isSet() should be true This will be non-null if this obs is a member of * another groupedObs * * @see #isObsGrouping() (??) */ protected Obs obsGroup; /** * The list of obs grouped under this obs. */ @AllowDirectAccess protected Set<Obs> groupMembers; protected Concept valueCoded; protected ConceptName valueCodedName; protected Drug valueDrug; protected Integer valueGroupId; protected Date valueDatetime; protected Double valueNumeric; protected String valueModifier; protected String valueText; protected String valueComplex; // ComplexData is not persisted in the database. protected transient ComplexData complexData; protected String comment; protected transient Integer personId; protected Person person; protected Order order; protected Location location; protected Encounter encounter; private Obs previousVersion; private String formNamespaceAndPath; private Boolean dirty = Boolean.FALSE; private Interpretation interpretation; private Status status = Status.FINAL; /** default constructor */ public Obs() { } /** * Required parameters constructor A value is also required, but that can be one of: valueCoded, * valueDrug, valueNumeric, or valueText * * @param person The Person this obs is acting on * @param question The question concept this obs is related to * @param obsDatetime The time this obs took place * @param location The location this obs took place */ public Obs(Person person, Concept question, Date obsDatetime, Location location) { this.person = person; if (person != null) { this.personId = person.getPersonId(); } this.concept = question; this.obsDatetime = obsDatetime; this.location = location; } /** constructor with id */ public Obs(Integer obsId) { this.obsId = obsId; } /** * This is an equivalent to a copy constructor. Creates a new copy of the given * <code>obsToCopy</code> with a null obs id * * @param obsToCopy The Obs that is going to be copied * @return a new Obs object with all the same attributes as the given obs */ public static Obs newInstance(Obs obsToCopy) { Obs newObs = new Obs(obsToCopy.getPerson(), obsToCopy.getConcept(), obsToCopy.getObsDatetime(), obsToCopy.getLocation()); newObs.setObsGroup(obsToCopy.getObsGroup()); newObs.setAccessionNumber(obsToCopy.getAccessionNumber()); newObs.setValueCoded(obsToCopy.getValueCoded()); newObs.setValueDrug(obsToCopy.getValueDrug()); newObs.setValueGroupId(obsToCopy.getValueGroupId()); newObs.setValueDatetime(obsToCopy.getValueDatetime()); newObs.setValueNumeric(obsToCopy.getValueNumeric()); newObs.setValueModifier(obsToCopy.getValueModifier()); newObs.setValueText(obsToCopy.getValueText()); newObs.setComment(obsToCopy.getComment()); newObs.setEncounter(obsToCopy.getEncounter()); newObs.setCreator(obsToCopy.getCreator()); newObs.setDateCreated(obsToCopy.getDateCreated()); newObs.setVoided(obsToCopy.getVoided()); newObs.setVoidedBy(obsToCopy.getVoidedBy()); newObs.setDateVoided(obsToCopy.getDateVoided()); newObs.setVoidReason(obsToCopy.getVoidReason()); newObs.setStatus(obsToCopy.getStatus()); newObs.setInterpretation(obsToCopy.getInterpretation()); newObs.setValueComplex(obsToCopy.getValueComplex()); newObs.setComplexData(obsToCopy.getComplexData()); newObs.setFormField(obsToCopy.getFormFieldNamespace(),obsToCopy.getFormFieldPath()); // Copy list of all members, including voided, and put them in respective groups if (obsToCopy.hasGroupMembers(true)) { for (Obs member : obsToCopy.getGroupMembers(true)) { // if the obs hasn't been saved yet, no need to duplicate it if (member.getObsId() == null) { newObs.addGroupMember(member); } else { Obs newMember = Obs.newInstance(member); newMember.setPreviousVersion(member); newObs.addGroupMember(newMember); } } } return newObs; } // Property accessors /** * @return Returns the comment. */ public String getComment() { return comment; } /** * @param comment The comment to set. */ public void setComment(String comment) { markAsDirty(this.comment, comment); this.comment = comment; } /** * @return Returns the concept. */ public Concept getConcept() { return concept; } /** * @param concept The concept to set. */ public void setConcept(Concept concept) { markAsDirty(this.concept, concept); this.concept = concept; } /** * Get the concept description that is tied to the concept name that was used when making this * observation * * @return ConceptDescription the description used */ public ConceptDescription getConceptDescription() { // if we don't have a question for this concept, // then don't bother looking for a description if (getConcept() == null) { return null; } // ABKTOD: description in which locale? return concept.getDescription(); } /** * @return Returns the encounter. */ public Encounter getEncounter() { return encounter; } /** * @param encounter The encounter to set. */ public void setEncounter(Encounter encounter) { markAsDirty(this.encounter, encounter); this.encounter = encounter; } /** * @return Returns the location. */ public Location getLocation() { return location; } /** * @param location The location to set. */ public void setLocation(Location location) { markAsDirty(this.location, location); this.location = location; } /** * @return Returns the obsDatetime. */ public Date getObsDatetime() { return obsDatetime; } /** * @param obsDatetime The obsDatetime to set. */ public void setObsDatetime(Date obsDatetime) { markAsDirty(this.obsDatetime, obsDatetime); this.obsDatetime = obsDatetime; } /** * An obs grouping occurs when the question (#getConcept()) is a set. (@link * org.openmrs.Concept#isSet()) If this is non-null, it means the current Obs is in the list * returned by <code>obsGroup</code>.{@link #getGroupMembers()} * * @return the Obs that is the grouping factor */ public Obs getObsGroup() { return obsGroup; } /** * This method does NOT add this current obs to the list of obs in obsGroup.getGroupMembers(). * That must be done (and should be done) manually. (I am not doing it here for fear of screwing * up the normal loading and creation of this object via hibernate/spring) * * @param obsGroup the obsGroup to set */ public void setObsGroup(Obs obsGroup) { markAsDirty(this.obsGroup, obsGroup); this.obsGroup = obsGroup; } /** * Convenience method that checks for if this obs has 1 or more group members (either voided or * non-voided) Note this method differs from hasGroupMembers(), as that method excludes voided * obs; logic is that while a obs that has only voided group members should be seen as * "having no group members" it still should be considered an "obs grouping" * <p> * NOTE: This method could also be called "isObsGroup" for a little less confusion on names. * However, jstl in a web layer (or any psuedo-getter) access isn't good with both an * "isObsGroup" method and a "getObsGroup" method. Which one should be returned with a * simplified jstl call like ${obs.obsGroup} ? With this setup, ${obs.obsGrouping} returns a * boolean of whether this obs is a parent and has members. ${obs.obsGroup} returns the parent * object to this obs if this obs is a group member of some other group. * * @return true if this is the parent group of other obs */ public boolean isObsGrouping() { return hasGroupMembers(true); } /** * A convenience method to check for nullity and length to determine if this obs has group * members. By default, this ignores voided-objects. To include voided, use * {@link #hasGroupMembers(boolean)} with value true. * * @return true if this is the parent group of other obs * @should not include voided obs */ public boolean hasGroupMembers() { return hasGroupMembers(false); } /** * Convenience method that checks for nullity and length to determine if this obs has group * members. The parameter specifies if this method whether or not voided obs should be * considered. * * @param includeVoided determines if Voided members should be considered as group members. * @return true if this is the parent group of other Obs * @should return true if this obs has group members based on parameter */ public boolean hasGroupMembers(boolean includeVoided) { // ! symbol used because if it's not empty, we want true return !org.springframework.util.CollectionUtils.isEmpty(getGroupMembers(includeVoided)); } /** * Get the non-voided members of the obs group, if this obs is a group. By default this method * only returns non-voided group members. To get all group members, use * {@link #getGroupMembers(boolean)} with value true. * <p> * If it's not a group (i.e. {@link #getConcept()}.{@link org.openmrs.Concept#getSet()} is not * true, then this returns null. * * @return a Set<Obs> of the members of this group. * @see #addGroupMember(Obs) * @see #hasGroupMembers() */ public Set<Obs> getGroupMembers() { return getGroupMembers(false); //same as just returning groupMembers } /** * Get the group members of this obs group, if this obs is a group. This method will either * return all group members, or only non-voided group members, depending on if the argument is * set to be true or false respectively. * * @param includeVoided * @return the set of group members in this obs group * @should Get all group members if passed true, and non-voided if passed false */ public Set<Obs> getGroupMembers(boolean includeVoided) { if (includeVoided) { //just return all group members return groupMembers; } if (groupMembers == null) { //Empty set so return null return null; } Set<Obs> nonVoided = new LinkedHashSet<Obs>(groupMembers); Iterator<Obs> i = nonVoided.iterator(); while (i.hasNext()) { Obs obs = i.next(); if (obs.getVoided()) { i.remove(); } } return nonVoided; } /** * Set the members of the obs group, if this obs is a group. * <p> * If it's not a group (i.e. {@link #getConcept()}.{@link org.openmrs.Concept#getSet()} is not * true, then this returns null. * * @param groupMembers the groupedObs to set * @see #addGroupMember(Obs) * @see #hasGroupMembers() * @should mark the obs as dirty when the set is changed from null to a non empty one * @should not mark the obs as dirty when the set is changed from null to an empty one * @should mark the obs as dirty when the set is replaced with another with different members * @should not mark the obs as dirty when the set is replaced with another with same members */ public void setGroupMembers(Set<Obs> groupMembers) { this.groupMembers = new HashSet<Obs>(groupMembers); //Copy over the entire list } /** * Convenience method to add the given <code>obs</code> to this grouping. Will implicitly make * this obs an ObsGroup. * * @param member Obs to add to this group * @see #setGroupMembers(Set) * @see #getGroupMembers() * @should return true when a new obs is added as a member * @should return false when a duplicate obs is added as a member */ public void addGroupMember(Obs member) { if (member == null) { return; } if (getGroupMembers() == null) { groupMembers = new HashSet<Obs>(); } // a quick sanity check to make sure someone isn't adding // itself to the group if (member.equals(this)) { throw new APIException("Obs.error.groupCannotHaveItselfAsAMentor", new Object[] { this, member }); } member.setObsGroup(this); groupMembers.add(member); } /** * Convenience method to remove an Obs from this grouping This also removes the link in the * given <code>obs</code>object to this obs grouper * * @param member Obs to remove from this group * @see #setGroupMembers(Set) * @see #getGroupMembers() * @should return true when an obs is removed * @should return false when a non existent obs is removed */ public void removeGroupMember(Obs member) { if (member == null || getGroupMembers() == null) { return; } if (groupMembers.remove(member)) { member.setObsGroup(null); } } /** * Convenience method that returns related Obs If the Obs argument is not an ObsGroup: a * Set<Obs> will be returned containing all of the children of this Obs' parent that are * not ObsGroups themselves. This will include this Obs by default, unless getObsGroup() returns * null, in which case an empty set is returned. If the Obs argument is an ObsGroup: a * Set<Obs> will be returned containing 1. all of this Obs' group members, and 2. all * ancestor Obs that are not themselves obsGroups. * * @return Set<Obs> */ public Set<Obs> getRelatedObservations() { Set<Obs> ret = new HashSet<Obs>(); if (this.isObsGrouping()) { ret.addAll(this.getGroupMembers()); Obs parentObs = this; while (parentObs.getObsGroup() != null) { for (Obs obsSibling : parentObs.getObsGroup().getGroupMembers()) { if (!obsSibling.isObsGrouping()) { ret.add(obsSibling); } } parentObs = parentObs.getObsGroup(); } } else if (this.getObsGroup() != null) { for (Obs obsSibling : this.getObsGroup().getGroupMembers()) { if (!obsSibling.isObsGrouping()) { ret.add(obsSibling); } } } return ret; } /** * @return Returns the obsId. */ public Integer getObsId() { return obsId; } /** * @param obsId The obsId to set. */ public void setObsId(Integer obsId) { this.obsId = obsId; } /** * @return Returns the order. */ public Order getOrder() { return order; } /** * @param order The order to set. */ public void setOrder(Order order) { markAsDirty(this.order, order); this.order = order; } /** * The person id of the person on this object. This should be the same as * <code>{@link #getPerson()}.getPersonId()</code>. It is duplicated here for speed and * simplicity reasons * * @return the integer person id of the person this obs is acting on */ public Integer getPersonId() { return personId; } /** * Set the person id on this obs object. This method is here for convenience, but really the * {@link #setPerson(Person)} method should be used like * <code>setPerson(new Person(personId))</code> * * @see #setPerson(Person) * @param personId */ protected void setPersonId(Integer personId) { markAsDirty(this.personId, personId); this.personId = personId; } /** * Get the person object that this obs is acting on. * * @see #getPersonId() * @return the person object */ public Person getPerson() { return person; } /** * Set the person object to this obs object. This will also set the personId on this obs object * * @see #setPersonId(Integer) * @param person the Patient/Person object that this obs is acting on */ public void setPerson(Person person) { markAsDirty(this.person, person); this.person = person; if (person != null) { setPersonId(person.getPersonId()); } } /** * Sets the value of this obs to the specified valueBoolean if this obs has a boolean concept. * * @param valueBoolean the boolean value matching the boolean coded concept to set to */ public void setValueBoolean(Boolean valueBoolean) { if (valueBoolean != null && getConcept() != null && getConcept().getDatatype().isBoolean()) { setValueCoded(valueBoolean.booleanValue() ? Context.getConceptService().getTrueConcept() : Context .getConceptService().getFalseConcept()); } else if (valueBoolean == null) { setValueCoded(null); } } /** * Coerces a value to a Boolean representation * * @return Boolean representation of the obs value * @should return true for value_numeric concepts if value is 1 * @should return false for value_numeric concepts if value is 0 * @should return null for value_numeric concepts if value is neither 1 nor 0 */ public Boolean getValueAsBoolean() { if (getValueCoded() != null) { if (getValueCoded().equals(Context.getConceptService().getTrueConcept())) { return Boolean.TRUE; } else if (getValueCoded().equals(Context.getConceptService().getFalseConcept())) { return Boolean.FALSE; } } else if (getValueNumeric() != null) { if (getValueNumeric() == 1) { return Boolean.TRUE; } else if (getValueNumeric() == 0) { return Boolean.FALSE; } } //returning null is preferred to defaulting to false to support validation of user input is from a form return null; } /** * Returns the boolean value if the concept of this obs is of boolean datatype * * @return true or false if value is set otherwise null * @should return true if value coded answer concept is true concept * @should return false if value coded answer concept is false concept */ public Boolean getValueBoolean() { if (getConcept() != null && valueCoded != null && getConcept().getDatatype().isBoolean()) { Concept trueConcept = Context.getConceptService().getTrueConcept(); return trueConcept != null && valueCoded.getId().equals(trueConcept.getId()); } return null; } /** * @return Returns the valueCoded. */ public Concept getValueCoded() { return valueCoded; } /** * @param valueCoded The valueCoded to set. */ public void setValueCoded(Concept valueCoded) { markAsDirty(this.valueCoded, valueCoded); this.valueCoded = valueCoded; } /** * Gets the specific name used for the coded value. * * @return the name of the coded value */ public ConceptName getValueCodedName() { return valueCodedName; } /** * Sets the specific name used for the coded value. * * @param valueCodedName the name of the coded value */ public void setValueCodedName(ConceptName valueCodedName) { markAsDirty(this.valueCodedName, valueCodedName); this.valueCodedName = valueCodedName; } /** * @return Returns the valueDrug */ public Drug getValueDrug() { return valueDrug; } /** * @param valueDrug The valueDrug to set. */ public void setValueDrug(Drug valueDrug) { markAsDirty(this.valueDrug, valueDrug); this.valueDrug = valueDrug; } /** * @return Returns the valueDatetime. */ public Date getValueDatetime() { return valueDatetime; } /** * @param valueDatetime The valueDatetime to set. */ public void setValueDatetime(Date valueDatetime) { markAsDirty(this.valueDatetime, valueDatetime); this.valueDatetime = valueDatetime; } /** * @return the value of this obs as a Date. Note that this uses a java.util.Date, so it includes * a time component, that should be ignored. * @since 1.9 */ public Date getValueDate() { return valueDatetime; } /** * @param valueDate The date value to set. * @since 1.9 */ public void setValueDate(Date valueDate) { markAsDirty(this.valueDatetime, valueDate); this.valueDatetime = valueDate; } /** * @return the time value of this obs. Note that this uses a java.util.Date, so it includes a * date component, that should be ignored. * @since 1.9 */ public Date getValueTime() { return valueDatetime; } /** * @param valueTime the time value to set * @since 1.9 */ public void setValueTime(Date valueTime) { markAsDirty(this.valueDatetime, valueTime); this.valueDatetime = valueTime; } /** * @return Returns the valueGroupId. */ public Integer getValueGroupId() { return valueGroupId; } /** * @param valueGroupId The valueGroupId to set. */ public void setValueGroupId(Integer valueGroupId) { markAsDirty(this.valueGroupId, valueGroupId); this.valueGroupId = valueGroupId; } /** * @return Returns the valueModifier. */ public String getValueModifier() { return valueModifier; } /** * @param valueModifier The valueModifier to set. */ public void setValueModifier(String valueModifier) { markAsDirty(this.valueModifier, valueModifier); this.valueModifier = valueModifier; } /** * @return Returns the valueNumeric. */ public Double getValueNumeric() { return valueNumeric; } /** * @param valueNumeric The valueNumeric to set. */ public void setValueNumeric(Double valueNumeric) { markAsDirty(this.valueNumeric, valueNumeric); this.valueNumeric = valueNumeric; } /** * @return Returns the valueText. */ public String getValueText() { return valueText; } /** * @param valueText The valueText to set. */ public void setValueText(String valueText) { markAsDirty(this.valueText, valueText); this.valueText = valueText; } /** * @return Returns true if this Obs is complex. * @since 1.5 * @should return true if the concept is complex */ public boolean isComplex() { // if (getValueComplex() != null) { // return true; // } if (getConcept() != null) { return getConcept().isComplex(); } return false; } /** * Get the value for the ComplexData. This method is used by the ComplexObsHandler. The * valueComplex has two parts separated by a bar '|' character: part A) the title; and part B) * the URI. The title is the readable description of the valueComplex that is returned by * {@link Obs#getValueAsString()}. The URI is the location where the ComplexData is stored. * * @return readable title and URI for the location of the ComplexData binary object. * @since 1.5 */ public String getValueComplex() { return this.valueComplex; } /** * Set the value for the ComplexData. This method is used by the ComplexObsHandler. The * valueComplex has two parts separated by a bar '|' character: part A) the title; and part B) * the URI. The title is the readable description of the valueComplex that is returned by * Obs.getValueAsString(). The URI is the location where the ComplexData is stored. * * @param valueComplex readable title and URI for the location of the ComplexData binary object. * @since 1.5 */ public void setValueComplex(String valueComplex) { markAsDirty(this.valueComplex, valueComplex); this.valueComplex = valueComplex; } /** * Set the ComplexData for this Obs. The ComplexData is stored in the file system or elsewhere, * but is not persisted to the database. <br> * <br> * {@link ComplexObsHandler}s that are registered to {@link ConceptComplex}s will persist the * {@link ComplexData#getData()} object to the correct place for the given concept. * * @param complexData * @since 1.5 */ public void setComplexData(ComplexData complexData) { markAsDirty(this.complexData, complexData); this.complexData = complexData; } /** * Get the ComplexData. This is retrieved by the {@link ComplexObsHandler} from the file system * or another location, not from the database. <br> * <br> * This will be null unless you call: * * <pre> * Obs obsWithComplexData = * Context.getObsService().getComplexObs(obsId, OpenmrsConstants.RAW_VIEW); * * <pre/> * * @return the complex data for this obs (if its a complex obs) * @since 1.5 */ public ComplexData getComplexData() { return this.complexData; } /** * @return Returns the accessionNumber. */ public String getAccessionNumber() { return accessionNumber; } /** * @param accessionNumber The accessionNumber to set. */ public void setAccessionNumber(String accessionNumber) { markAsDirty(this.accessionNumber, accessionNumber); this.accessionNumber = accessionNumber; } /*************************************************************************** * Convenience methods **************************************************************************/ /** * Convenience method for obtaining the observation's value as a string If the Obs is complex, * returns the title of the complexData denoted by the section of getValueComplex() before the * first bar '|' character; or returns the entire getValueComplex() if the bar '|' character is * missing. * * @param locale locale for locale-specific depictions of value * @should return first part of valueComplex for complex obs * @should return first part of valueComplex for non null valueComplexes * @should return non precise values for NumericConcepts * @should return date in correct format * @should not return long decimal numbers as scientific notation * @should use commas or decimal places depending on locale * @should not use thousand separator * @should return regular number for size of zero to or greater than ten digits * @should return regular number if decimal places are as high as six */ public String getValueAsString(Locale locale) { // formatting for the return of numbers of type double NumberFormat nf = NumberFormat.getNumberInstance(locale); DecimalFormat df = (DecimalFormat) nf; df.applyPattern("#0.0#####"); // formatting style up to 6 digits //branch on hl7 abbreviations if (getConcept() != null) { String abbrev = getConcept().getDatatype().getHl7Abbreviation(); if ("BIT".equals(abbrev)) { return getValueAsBoolean() == null ? "" : getValueAsBoolean().toString(); } else if ("CWE".equals(abbrev)) { if (getValueCoded() == null) { return ""; } if (getValueDrug() != null) { return getValueDrug().getFullName(locale); } else { ConceptName valueCodedName = getValueCodedName(); if (valueCodedName != null) { return getValueCoded().getName(locale, false).getName(); } else { ConceptName fallbackName = getValueCoded().getName(); if (fallbackName != null) { return fallbackName.getName(); } else { return ""; } } } } else if ("NM".equals(abbrev) || "SN".equals(abbrev)) { if (getValueNumeric() == null) { return ""; } else { if (getConcept() instanceof ConceptNumeric) { ConceptNumeric cn = (ConceptNumeric) getConcept(); if (!cn.getAllowDecimal()) { double d = getValueNumeric(); int i = (int) d; return Integer.toString(i); } else { df.format(getValueNumeric()); } } } } else if ("DT".equals(abbrev)) { DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN); return (getValueDatetime() == null ? "" : dateFormat.format(getValueDatetime())); } else if ("TM".equals(abbrev)) { return (getValueDatetime() == null ? "" : Format.format(getValueDatetime(), locale, FORMAT_TYPE.TIME)); } else if ("TS".equals(abbrev)) { return (getValueDatetime() == null ? "" : Format.format(getValueDatetime(), locale, FORMAT_TYPE.TIMESTAMP)); } else if ("ST".equals(abbrev)) { return getValueText(); } else if ("ED".equals(abbrev) && getValueComplex() != null) { String[] valueComplex = getValueComplex().split("\\|"); for (int i = 0; i < valueComplex.length; i++) { if (StringUtils.isNotEmpty(valueComplex[i])) { return valueComplex[i].trim(); } } } } // if the datatype is 'unknown', default to just returning what is not null if (getValueNumeric() != null) { return df.format(getValueNumeric()); } else if (getValueCoded() != null) { if (getValueDrug() != null) { return getValueDrug().getFullName(locale); } else { ConceptName valudeCodedName = getValueCodedName(); if (valudeCodedName != null) { return valudeCodedName.getName(); } else { return ""; } } } else if (getValueDatetime() != null) { return Format.format(getValueDatetime(), locale, FORMAT_TYPE.DATE); } else if (getValueText() != null) { return getValueText(); } else if (hasGroupMembers()) { // all of the values are null and we're an obs group...so loop // over the members and just do a getValueAsString on those // this could potentially cause an infinite loop if an obs group // is a member of its own group at some point in the hierarchy StringBuilder sb = new StringBuilder(); for (Obs groupMember : getGroupMembers()) { if (sb.length() > 0) { sb.append(", "); } sb.append(groupMember.getValueAsString(locale)); } return sb.toString(); } // returns the title portion of the valueComplex // which is everything before the first bar '|' character. if (getValueComplex() != null) { String[] valueComplex = getValueComplex().split("\\|"); for (int i = 0; i < valueComplex.length; i++) { if (StringUtils.isNotEmpty(valueComplex[i])) { return valueComplex[i].trim(); } } } return ""; } /** * Sets the value for the obs from a string depending on the datatype of the question concept * * @param s the string to coerce to a boolean * @should set value as boolean if the datatype of the question concept is boolean * @should fail if the value of the string is null * @should fail if the value of the string is empty */ public void setValueAsString(String s) throws ParseException { if (log.isDebugEnabled()) { log.debug("getConcept() == " + getConcept()); } if (getConcept() != null && !StringUtils.isBlank(s)) { String abbrev = getConcept().getDatatype().getHl7Abbreviation(); if ("BIT".equals(abbrev)) { setValueBoolean(Boolean.valueOf(s)); } else if ("CWE".equals(abbrev)) { throw new RuntimeException("Not Yet Implemented"); } else if ("NM".equals(abbrev) || "SN".equals(abbrev)) { setValueNumeric(Double.valueOf(s)); } else if ("DT".equals(abbrev)) { DateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN); setValueDatetime(dateFormat.parse(s)); } else if ("TM".equals(abbrev)) { DateFormat timeFormat = new SimpleDateFormat(TIME_PATTERN); setValueDatetime(timeFormat.parse(s)); } else if ("TS".equals(abbrev)) { DateFormat datetimeFormat = new SimpleDateFormat(DATE_TIME_PATTERN); setValueDatetime(datetimeFormat.parse(s)); } else if ("ST".equals(abbrev)) { setValueText(s); } else { throw new RuntimeException("Don't know how to handle " + abbrev); } } else { throw new RuntimeException("concept is null for " + this); } } /** * @see java.lang.Object#toString() */ @Override public String toString() { if (obsId == null) { return "obs id is null"; } return "Obs #" + obsId.toString(); } /** * @since 1.5 * @see org.openmrs.OpenmrsObject#getId() */ @Override public Integer getId() { return getObsId(); } /** * @since 1.5 * @see org.openmrs.OpenmrsObject#setId(java.lang.Integer) */ @Override public void setId(Integer id) { setObsId(id); } /** * When ObsService updates an obs, it voids the old version, creates a new Obs with the updates, * and adds a reference to the previousVersion in the new Obs. getPreviousVersion returns the * last version of this Obs. */ public Obs getPreviousVersion() { return previousVersion; } /** * A previousVersion indicates that this Obs replaces an earlier one. * * @param previousVersion the Obs that this Obs superceeds */ public void setPreviousVersion(Obs previousVersion) { markAsDirty(this.previousVersion, previousVersion); this.previousVersion = previousVersion; } public Boolean hasPreviousVersion() { return getPreviousVersion() != null; } /** * @param creator * @see Auditable#setCreator(User) */ @Override public void setCreator(User creator) { markAsDirty(getCreator(), creator); super.setCreator(creator); } /** * @param dateCreated * @see Auditable#setDateCreated(Date) */ @Override public void setDateCreated(Date dateCreated) { markAsDirty(getDateCreated(), dateCreated); super.setDateCreated(dateCreated); } /** * Gets the namespace for the form field that was used to capture the obs details in the form * * @return the namespace * @since 1.11 * @should return the namespace for a form field that has no path * @should return the correct namespace for a form field with a path * @should return null if the namespace is not specified */ public String getFormFieldNamespace() { if (StringUtils.isNotBlank(formNamespaceAndPath)) { //Only the path was specified if (formNamespaceAndPath.startsWith(FORM_NAMESPACE_PATH_SEPARATOR)) { return null; } return formNamespaceAndPath.substring(0, formNamespaceAndPath.indexOf(FORM_NAMESPACE_PATH_SEPARATOR)); } return formNamespaceAndPath; } /** * Gets the path for the form field that was used to capture the obs details in the form * * @return the the form field path * @since 1.11 * @should return the path for a form field that has no namespace * @should return the correct path for a form field with a namespace * @should return null if the path is not specified */ public String getFormFieldPath() { if (StringUtils.isNotBlank(formNamespaceAndPath)) { //Only the namespace was specified if (formNamespaceAndPath.endsWith(FORM_NAMESPACE_PATH_SEPARATOR)) { return null; } return formNamespaceAndPath.substring(formNamespaceAndPath.indexOf(FORM_NAMESPACE_PATH_SEPARATOR) + 1); } return formNamespaceAndPath; } /** * Sets the namespace and path of the form field that was used to capture the obs details in the * form.<br> * <b>Note:</b> Namespace and formFieldPath together must not exceed 254 characters in length, * form applications can subtract the length of their namespace from 254 to determine the * maximum length they can use for a form field path. * * @param namespace the namespace of the form field * @param formFieldPath the path of the form field * @since 1.11 * @should set the underlying formNamespaceAndPath in the correct pattern * @should reject a namepace containing the separator * @should reject a path containing the separator * @should reject a namepace and path combination longer than the max length * @should not mark the obs as dirty when the value has not been changed * @should mark the obs as dirty when the value has been changed * @should mark the obs as dirty when the value is changed from a null to a non null value * @should mark the obs as dirty when the value is changed from a non null to a null value */ public void setFormField(String namespace, String formFieldPath) { if (namespace == null && formFieldPath == null) { markAsDirty(formNamespaceAndPath, null); formNamespaceAndPath = null; return; } String nsAndPathTemp = ""; if (StringUtils.isNotBlank(namespace) && StringUtils.isNotBlank(formFieldPath)) { nsAndPathTemp = namespace + FORM_NAMESPACE_PATH_SEPARATOR + formFieldPath; } else if (StringUtils.isNotBlank(namespace)) { nsAndPathTemp = namespace + FORM_NAMESPACE_PATH_SEPARATOR; } else if (StringUtils.isNotBlank(formFieldPath)) { nsAndPathTemp = FORM_NAMESPACE_PATH_SEPARATOR + formFieldPath; } if (nsAndPathTemp.length() > FORM_NAMESPACE_PATH_MAX_LENGTH) { throw new APIException("Obs.namespaceAndPathTooLong", (Object[]) null); } if (StringUtils.countMatches(nsAndPathTemp, FORM_NAMESPACE_PATH_SEPARATOR) > 1) { throw new APIException("Obs.namespaceAndPathNotContainSeparator", (Object[]) null); } markAsDirty(this.formNamespaceAndPath, nsAndPathTemp); formNamespaceAndPath = nsAndPathTemp; } /** * Returns true if any change has been made to an Obs instance. In general, the only time * isDirty() is going to return false is when a new Obs has just been instantiated or loaded * from the database and no method that modifies it internally has been invoked. * * @return true if not changed otherwise false * @since 2.0 * @should return false when no change has been made * @should return true when any immutable field has been changed * @should return false when only mutable fields are changed * @should return true when an immutable field is changed from a null to a non null value * @should return true when an immutable field is changed from a non null to a null value */ public boolean isDirty() { return dirty; } private void markAsDirty(Object oldValue, Object newValue) { //Should we ignore the case for Strings? if (!isDirty() && obsId != null && !OpenmrsUtil.nullSafeEquals(oldValue, newValue)) { dirty = true; } } /** * Similar to FHIR's Observation.interpretation. Supports a subset of FHIR's Observation Interpretation Codes. * See https://www.hl7.org/fhir/valueset-observation-interpretation.html * @since 2.1.0 */ public Interpretation getInterpretation() { return interpretation; } /** * @since 2.1.0 */ public void setInterpretation(Interpretation interpretation) { markAsDirty(this.interpretation, interpretation); this.interpretation = interpretation; } /** * Similar to FHIR's Observation.status. Supports a subset of FHIR's ObservationStatus values. * At present OpenMRS does not support FHIR's REGISTERED and CANCELLED statuses, because we don't support obs with * null values. * See: https://www.hl7.org/fhir/valueset-observation-status.html * @since 2.1.0 */ public Status getStatus() { return status; } /** * @since 2.1.0 */ public void setStatus(Status status) { markAsDirty(this.status, status); this.status = status; } }