Java tutorial
/** * 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.logic.result; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.builder.HashCodeBuilder; 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 summary="Return logic"> * <tr> * <th>Datatype</th> * <th>Returns</th> * </tr> * <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 summary="Return logic"> * <tr> * <th>Datatype</th> * <th>Returns</th> * </tr> * <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 summary="Return logic"> * <tr> * <th>Datatype</th> * <th>Returns</th> * </tr> * <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> * <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 : Long.valueOf(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 = Integer.valueOf(1); Map<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; } @Override public int hashCode() { if (isSingleResult()) { return new HashCodeBuilder().append(datatype).hashCode(); } else { return super.hashCode(); } } /** * @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); } }