/** * The contents of this file are subject to the OpenMRS Public License * Version 1.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://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.logic.result; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import org.openmrs.Concept; import org.openmrs.ConceptDatatype; import org.openmrs.Obs; import org.openmrs.api.context.Context; import org.openmrs.logic.LogicException; /** * * A result from the logic service. A result can be 0-to-n date-values pairs. * You can treat the result as a list or easily coerce it into a simple value as * needed. <br/><br/> * * When possible, results carry references to more complex objects so that code * that deals with results and has some prior knowledge of the objects returned * by a particular rule can more easily get to the full-featured objects instead * of the simplified values in the date-value pairs.<br/><br/> * * TODO: eliminate unnecessary methods (toDatetime(), getDatetime(), and * getDate() should all do the same thing)<br/> * * TODO: better support/handling of NULL_RESULT * */ public class Result extends ArrayList<Result> { private static final long serialVersionUID = -5587574403423820797L; /** * Core datatypes for a result. Each result is one of these datatypes, but can be easily coerced * into the other datatypes. To promote flexibility and maximize re-usability of logic rules, * the value of a result can be controlled individually for each datatype — i.e., specfic * datatype representations of a single result can be overridden. For example, a result could * have a <em>numeric</em> value of 0.15 and its text value could be overridden to be * "15 percent" or "Fifteen percent." */ public enum Datatype { /** * Represents a true/false type of result */ BOOLEAN, /** * Represents a Concept type of result */ CODED, /** * Represents a date type of result */ DATETIME, /** * Represents number (float, double, int) type of results */ NUMERIC, /** * Represents string type of results */ TEXT } private Datatype datatype; private Date resultDatetime; private Boolean valueBoolean; private Concept valueCoded; private Date valueDatetime; private Double valueNumeric; private String valueText; private Object resultObject; private static final Result emptyResult = new EmptyResult(); public Result() { } /** * Builds result upon another result — the first step in create a result that contains a * list of other results. * * @param result the result that will be the sole member of the new result * @should not fail with null result */ public Result(Result result) { if (result != null) { this.add(result); } } /** * Builds a result from a list of results * * @param list a list of results * @should not fail with null list * @should not fail with empty list */ public Result(List<Result> list) { if (!(list == null || list.size() < 1)) this.addAll(list); } /** * Builds a boolean result with a result date of today * * @param valueBoolean */ public Result(Boolean valueBoolean) { this(new Date(), valueBoolean, null); } /** * Builds a boolean result with a specific result date * * @param resultDate * @param valueBoolean */ public Result(Date resultDate, Boolean valueBoolean, Object obj) { this(resultDate, Datatype.BOOLEAN, valueBoolean, null, null, null, null, obj); } /** * Builds a coded result with a result date of today * * @param valueCoded */ public Result(Concept valueCoded) { this(new Date(), valueCoded, null); } /** * Builds a coded result with a specific result date * * @param resultDate * @param valueCoded */ public Result(Date resultDate, Concept valueCoded, Object obj) { this(resultDate, Datatype.CODED, null, valueCoded, null, null, null, obj); } /** * Builds a coded result from an observation * * @param obs */ public Result(Obs obs) { this(obs.getObsDatetime(), null, obs.getValueAsBoolean(), obs.getValueCoded(), obs.getValueDatetime(), obs .getValueNumeric(), obs.getValueText(), obs); Concept concept = obs.getConcept(); ConceptDatatype conceptDatatype = null; if (concept != null) { conceptDatatype = concept.getDatatype(); if (conceptDatatype == null) { return; } if (conceptDatatype.isCoded()) this.datatype = Datatype.CODED; else if (conceptDatatype.isNumeric()) this.datatype = Datatype.NUMERIC; else if (conceptDatatype.isDate()) this.datatype = Datatype.DATETIME; else if (conceptDatatype.isText()) this.datatype = Datatype.TEXT; else if (conceptDatatype.isBoolean()) this.datatype = Datatype.BOOLEAN; } } /** * Builds a datetime result with a result date of today * * @param valueDatetime */ public Result(Date valueDatetime) { this(new Date(), valueDatetime, null); } /** * Builds a datetime result with a specific result date * * @param resultDate * @param valueDatetime */ public Result(Date resultDate, Date valueDatetime, Object obj) { this(resultDate, Datatype.DATETIME, null, null, valueDatetime, null, null, obj); } /** * Builds a numeric result with a result date of today * * @param valueNumeric */ public Result(Double valueNumeric) { this(new Date(), valueNumeric, null); } /** * Builds a numeric result with a specific result date * * @param resultDate * @param valueNumeric */ public Result(Date resultDate, Double valueNumeric, Object obj) { this(resultDate, Datatype.NUMERIC, null, null, null, valueNumeric, null, obj); } /** * Builds a numeric result with a result date of today * * @param valueNumeric */ public Result(Integer valueNumeric) { this(new Date(), valueNumeric, null); } /** * Builds a numeric result with a specific result date * * @param resultDate * @param valueNumeric */ public Result(Date resultDate, Integer valueNumeric, Object obj) { this(resultDate, Datatype.NUMERIC, null, null, null, valueNumeric.doubleValue(), null, obj); } /** * Builds a text result with a result date of today * * @param valueText */ public Result(String valueText) { this(new Date(), valueText, null); } /** * Builds a text result with a specific result date * * @param resultDate * @param valueText */ public Result(Date resultDate, String valueText, Object obj) { this(resultDate, Datatype.TEXT, null, null, null, null, valueText, obj); } /** * Builds a result date with specific (overloaded) values — i.e., instead of simply * accepting the default translation of one datatype into another (e.g., a date translated * automatically into string format), this contructor allows the various datatype * representations of the result to be individually controlled. Any values set to <em>null</em> * will yield the natural translation of the default datatype. For example, * * <pre> * Result result = new Result(new Date(), 2.5); * assertEqualtes("2.5", result.toString()); * * Result result = new Result(new Date(), * Result.Datatype.NUMERIC, * 2.5, * null, * null, * "Two and a half", * null); * assertEquals("Two and a half", result.toString()); * </pre> * * @param resultDate * @param datatype * @param valueBoolean * @param valueCoded * @param valueDatetime * @param valueNumeric * @param valueText * @param object */ public Result(Date resultDate, Datatype datatype, Boolean valueBoolean, Concept valueCoded, Date valueDatetime, Double valueNumeric, String valueText, Object object) { this.resultDatetime = resultDate; this.valueNumeric = valueNumeric; this.valueDatetime = valueDatetime; this.valueCoded = valueCoded; this.valueText = valueText; this.valueBoolean = valueBoolean; this.datatype = datatype; this.resultObject = object; } @Deprecated public static final Result nullResult() { return emptyResult; } /** * @return null/empty result */ public static final Result emptyResult() { return emptyResult; } /** * Returns the datatype of the result. If the result is a list of other results, then the * datatype of the first element is returned * * @return datatype of the result */ public Datatype getDatatype() { if (isSingleResult()) return this.datatype; // TODO: better option than defaulting to first element's datatype? return this.get(0).getDatatype(); } /** * Changes the result date time — not to be confused with a value that is a date. The * result date time is typically the datetime that the observation was recorded. * * @param resultDatetime */ public void setResultDate(Date resultDatetime) { this.resultDatetime = resultDatetime; } /** * Changes the default datatype of the result * * @param datatype */ public void setDatatype(Datatype datatype) { this.datatype = datatype; } /** * Overrides the boolean representation of ths result without changing the default datatype * * @param valueBoolean */ public void setValueBoolean(Boolean valueBoolean) { this.valueBoolean = valueBoolean; } /** * Overrides the coded representation of ths result without changing the default datatype * * @param valueCoded */ public void setValueCoded(Concept valueCoded) { this.valueCoded = valueCoded; } /** * Overrides the datetime representation of ths result without changing the default datatype * * @param valueDatetime */ public void setValueDatetime(Date valueDatetime) { this.valueDatetime = valueDatetime; } /** * Overrides the numeric representation of ths result without changing the default datatype * * @param valueNumeric */ public void setValueNumeric(Integer valueNumeric) { this.valueNumeric = valueNumeric.doubleValue(); } /** * Overrides the numeric representation of ths result without changing the default datatype * * @param valueNumeric */ public void setValueNumeric(Double valueNumeric) { this.valueNumeric = valueNumeric; } /** * Overrides the text representation of ths result without changing the default datatype * * @param valueText */ public void setValueText(String valueText) { this.valueText = valueText; } /** * Returns the data of the result (not to be confused with a data value). For example, if a * result represents an observation like DATE STARTED ON HIV TREATMENT, the <em>result date</em> * (returned by this method) would be the date the observation was recorded while the * <em>toDatetime()</em> method would be used to get the actual answer (when the patient started * their treatment). * * @return date of the result (usually the date the result was recorded or observed) * @see #toDatetime() */ public Date getResultDate() { if (isSingleResult()) return resultDatetime; return this.get(0).getResultDate(); } /** * Get the result object * * @return the underlying result object */ public Object getResultObject() { return this.resultObject; } /** * Set the result object * * @param object */ public void setResultObject(Object object) { this.resultObject = object; } /** * @return boolean representation of the result. For non-boolean results, this will either be * the overridden boolean value (if specifically defined) or a boolean representation of * the default datatype. If the result is a list, then return false only if all members * are false * <table> * <th> * <td>Datatype</td> * <td>Returns</td></th> * <tr> * <td>CODED</td> * <td>false for concept FALSE<br> * true for all others</td> * </tr> * <tr> * <td>DATETIME</td> * <td>true for any date value<br> * false if the date is null</td> * </tr> * <tr> * <td>NUMERIC</td> * <td>true for any non-zero number<br> * false for zero</td> * </tr> * <tr> * <td>TEXT</td> * <td>true for any non-blank value<br> * false if blank or null</td> * </tr> * </table> */ public Boolean toBoolean() { if (isSingleResult()) { if (datatype == null) { return valueBoolean; } switch (datatype) { case BOOLEAN: return (valueBoolean == null ? false : valueBoolean); case CODED: return (valueCoded == null ? false : true); // TODO: return // false for "FALSE" // concept case DATETIME: return (valueDatetime == null ? false : true); case NUMERIC: return (valueNumeric == null || valueNumeric == 0 ? false : true); case TEXT: return (valueText == null || valueText.length() < 1 ? false : true); default: return valueBoolean; } } for (Result r : this) if (!r.toBoolean()) return false; return true; } /** * @return concept for result. For non-concept results, returns the concept value if it was * overridden (specifically defined for the result), otherwise returns <em>null</em>. If * the result is a list, then the concept for the first member is returned. */ public Concept toConcept() { if (isSingleResult()) return valueCoded; return this.get(0).toConcept(); } /** * @return the datetime representation of the result <em>value</em> (not to be confused with the * result's own datetime). For non-datetime results, this will return the overridden * datetime value (if specifically defined) or datetime representation of the default * datatype. If the result is a list, then the datetime representation of the first * member is returned. * <table> * <th> * <td>Datatype</td> * <td>Returns</td></th> * <tr> * <td>BOOLEAN</td> * <td>null</td> * </tr> * <tr> * <td>CODED</td> * <td>null</td> * </tr> * <tr> * <td>NUMERIC</td> * <td>null</td> * </tr> * <tr> * <td>TEXT</td> * <td>If the text can be parsed into a date, then that value is returned;<br> * otherwise returns <em>null</em></td> * </tr> * </table> */ public Date toDatetime() { if (isSingleResult()) { if (valueDatetime != null) return valueDatetime; if (datatype == Datatype.TEXT && valueText != null) { try { return Context.getDateFormat().parse(valueText); } catch (Exception e) {} } return valueDatetime; } return this.get(0).toDatetime(); } /** * @return numeric representation of the result. For non-numeric results, this will either be * the overridden numeric value (if specifically defined) or a numeric representation of * the default datatype. If the result is a list, then the value of the first element is * returned. * <table> * <th> * <td>Datatype</td> * <td>Returns</td></th> * <tr> * <td>BOOLEAN</td> * <td>1 for true<br> * 0 for false</td> * </tr> * <tr> * <td>CODED</td> * <td>zero (0)</td> * </tr> * <tr> * <tr> * <td>DATETIME</td> * <td>Number of milliseconds since Java's epoch</td> * </tr> * <td>TEXT</td> * <td>numeric value of text if it can be parsed into a number<br> * otherwise zero (0)</td> </tr> * </table> */ public Double toNumber() { if (isSingleResult()) { if (datatype == null) { return valueNumeric; } switch (datatype) { case BOOLEAN: return (valueBoolean == null || !valueBoolean ? 0D : 1D); case CODED: return 0D; case DATETIME: return (valueDatetime == null ? 0 : new Long(valueDatetime.getTime()).doubleValue()); case NUMERIC: return (valueNumeric == null ? 0D : valueNumeric); case TEXT: try { return Double.parseDouble(valueText); } catch (Exception e) { return 0D; } default: return valueNumeric; } } return this.get(0).toNumber(); } /** * @return string representation of the result. For non-text results, this will either be the * overridden text value (if specifically defined) or a string representation of the * default datatype value. If the result is a list, then the string representation of * all members a joined with commas. */ public String toString() { if (isSingleResult()) { if (datatype == null) { return valueText == null ? "" : valueText; } switch (datatype) { case BOOLEAN: return (valueBoolean ? "true" : "false"); case CODED: return (valueCoded == null ? "" : valueCoded.getBestName(Context.getLocale()).getName()); case DATETIME: return (valueDatetime == null ? "" : Context.getDateFormat().format(valueDatetime)); case NUMERIC: return (valueNumeric == null ? "" : String.valueOf(valueNumeric)); case TEXT: return (valueText == null ? "" : valueText); default: return valueText; } } StringBuffer s = new StringBuffer(); for (Result r : this) { if (s.length() > 0) s.append(","); s.append(r.toString()); } return s.toString(); } /** * @return the object associated with the result (generally, this is used internally or for * advanced rule design) * @should return resultObject for single results * @should return all results for result list */ public Object toObject() { if (isSingleResult()) return resultObject; if (this.size() == 1) return this.get(0).toObject(); throw new LogicException("This result represents more than one result, you cannot call toObject on multiple results"); } /** * @return true if result is empty */ public boolean isNull() { return false; //EmptyResult has its own implementation //that should return true } /** * @return true if the result has any non-zero, non-empty value */ public boolean exists() { if (isSingleResult()) { return ((valueBoolean != null && valueBoolean) || valueCoded != null || valueDatetime != null || (valueNumeric != null && valueNumeric != 0) || (valueText != null && valueText.length() > 0)); } for (Result r : this) { if (r.exists()) return true; } return false; } public boolean contains(Concept concept) { return containsConcept(concept.getConceptId()); } /** * @return all results greater than the given value */ public Result gt(Integer value) { if (isSingleResult()) { if (valueNumeric == null || valueNumeric <= value) return emptyResult; return this; } List<Result> matches = new ArrayList<Result>(); for (Result r : this) { if (!r.gt(value).isEmpty()) matches.add(r); } if (matches.size() < 1) return emptyResult; return new Result(matches); } /** * @return true if result contains a coded value with the given concept id (if the result is a * list, then returns true if <em>any</em> member has a matching coded value) */ public boolean containsConcept(Integer conceptId) { if (isSingleResult()) return (valueCoded != null && valueCoded.getConceptId().equals(conceptId)); for (Result r : this) { if (r.containsConcept(conceptId)) return true; } return false; } /** * @return true if the result is equal to the given result or is a list containing a member * equal to the given result */ public boolean contains(Result result) { if (isSingleResult()) return this.equals(result); for (Result r : this) { if (r.contains(result)) return true; } return false; } /** * @return a result with all duplicates removed */ public Result unique() { if (isSingleResult()) return this; Integer something = new Integer(1); HashMap<Result, Integer> map = new HashMap<Result, Integer>(); for (Result r : this) map.put(r, something); List<Result> uniqueList = new ArrayList<Result>(map.keySet()); return new Result(uniqueList); } //TODO rewrite this method // // /** // * @see java.lang.Object#hashCode() // */ // public int hashCode() { // int hashCode = 49867; // some random number // hashCode += this.hashCode(); // // return hashCode; // } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof Result)) return false; Result r = (Result) obj; if (EmptyResult.class.isAssignableFrom(r.getClass()) && this.isEmpty()) { return true; } if (EmptyResult.class.isAssignableFrom(this.getClass()) && r.isEmpty()) { return true; } if (isSingleResult() && r.isSingleResult()) { if (datatype == null) { return false; } // both are single results switch (datatype) { case BOOLEAN: return (valueBoolean.equals(r.valueBoolean)); case CODED: return (valueCoded.equals(r.valueCoded)); case DATETIME: return (valueDatetime.equals(r.valueDatetime)); case NUMERIC: return (valueNumeric.equals(r.valueNumeric)); case TEXT: return (valueText.equals(r.valueText)); default: return false; } } if (isSingleResult() || r.isSingleResult()) // we already know they're not both single results, so if one is // single, it's not a match return false; if (this.size() != r.size()) return false; // at this point, we have two results that are lists, so members must // match exactly for (int i = 0; i < this.size(); i++) { if (!this.get(i).equals(r.get(i))) return false; } return true; } /** * @return the <em>index</em> element of a list. If the result is not a list, then this will * return the result only if <em>index</em> is equal to zero (0); otherwise, returns an * empty result * @see java.util.List#get(int) * @should get empty result for indexes out of range */ @Override public Result get(int index) { if (isSingleResult()) return (index == 0 ? this : emptyResult); if (index >= this.size()) { return emptyResult; } return super.get(index); } /** * @return the chronologically (based on result date) first result * @should get the first result given multiple results * @should get the result given a single result * @should get an empty result given an empty result * @should not get the result with null result date given other results * @should get one result with null result dates for all results */ public Result earliest() { if (isSingleResult()) return this; Result first = emptyResult(); // default the returned result to the first item // in case all resultDates are null if (size() > 0) first = get(0); for (Result r : this) { if (r != null && r.getResultDate() != null && (first.getResultDate() == null || r.getResultDate().before(first.getResultDate()))) { first = r; } } return first; } /** * @return the chronologically (based on result date) last result * @should get the most recent result given multiple results * @should get the result given a single result * @should get an empty result given an empty result * @should get the result with null result date */ public Result latest() { if (isSingleResult()) return this; Result last = emptyResult(); // default the returned result to the first item // in case all resultDates are null if (size() > 0) last = get(0); for (Result r : this) { if ((last.getResultDate() == null || (r.getResultDate() != null && r.getResultDate().after(last.getResultDate())))) { last = r; } } return last; } /** * Convenience method to know if this Result represents multiple results or not * * @return true/false whether this is just one Result or more than one */ private boolean isSingleResult() { return (this.size() < 1); } }