/**
* 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.reporting;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.api.PatientSetService;
import org.openmrs.api.PatientSetService.BooleanOperator;
import org.openmrs.cohort.CohortDefinition;
import org.openmrs.cohort.CohortSearchHistory;
import org.openmrs.cohort.CohortUtil;
import org.openmrs.report.EvaluationContext;
import org.openmrs.report.Parameter;
import org.openmrs.util.OpenmrsUtil;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Root;
/**
* This class represents a search for a set of patients, as entered from a user interface. There are
* different types of searches:
* <ul>
* <li>a composition, e.g. "1 and (2 or 3)"
* <li>a reference to a saved filter, expressed as the database integer pk.
* <li>a reference to a saved cohort, expressed as the database integer pk.
* <li>a regular search, which describes a PatientFilter subclass and a list of bean-style
* properties to set.
* </ul>
* Composition filters:<br/>
* When isComposition() returns true, then this represents something like "1 and (2 or 3)", which
* must be evaluated in the context of a search history.
* <p>
* Saved filters:<br/>
* When isSavedFilterReference() returns true, then this represents something like "saved filter #8"
* <br/>
* When isSavedCohortReference() returns true, then this represents something like "saved cohort #3"
* <p>
* Regular filters:<br/>
* Otherwise this search describes a PatientFilter subclass and a list of bean-style properties to
* set, so that it can be turned into a PatientFilter with the utility method
* OpenmrsUtil.toPatientFilter(PatientSearch). But it can also be left as-is for better
* version-compatibility if PatientFilter classes change, or to avoid issues with xml-encoding
* hibernate proxies.
*
* @deprecated see reportingcompatibility module
*/
@Root(strict = false)
@Deprecated
public class PatientSearch implements CohortDefinition {
private static final long serialVersionUID = -8913742497675209159L;
protected static final Log log = LogFactory.getLog(PatientSearch.class);
private static Set<String> andWords = new HashSet<String>();
private static Set<String> orWords = new HashSet<String>();
private static Set<String> notWords = new HashSet<String>();
private static Set<String> openParenthesesWords = new HashSet<String>();
private static Set<String> closeParenthesesWords = new HashSet<String>();
static {
andWords.add("and");
andWords.add("intersection");
andWords.add("*");
orWords.add("or");
orWords.add("union");
orWords.add("+");
notWords.add("not");
notWords.add("!");
openParenthesesWords.add("(");
openParenthesesWords.add("[");
openParenthesesWords.add("{");
closeParenthesesWords.add(")");
closeParenthesesWords.add("]");
closeParenthesesWords.add("}");
}
private Class<PatientFilter> filterClass;
private List<SearchArgument> arguments;
private List<Object> parsedComposition;
private Integer savedSearchId;
private Integer savedFilterId;
private Integer savedCohortId;
// Temporary storage for user-specified parameter values. This is a bit of a hack.
private transient Map<String, String> parameterValues = new HashMap<String, String>();
// static factory methods:
public static PatientSearch createSavedSearchReference(int id) {
PatientSearch ps = new PatientSearch();
ps.setSavedSearchId(id);
return ps;
}
public static PatientSearch createSavedFilterReference(int id) {
PatientSearch ps = new PatientSearch();
ps.setSavedFilterId(id);
return ps;
}
public static PatientSearch createSavedCohortReference(int id) {
PatientSearch ps = new PatientSearch();
ps.setSavedCohortId(id);
return ps;
}
public static PatientSearch createCompositionSearch(String description) {
// TODO This is a rewrite of the code in CohortSearchHistory.createCompositionFilter(String). That method should probably delegate to this one in some way.
// TODO use open/closeParenthesesWords declared above
List<Object> tokens = new ArrayList<Object>();
try {
StreamTokenizer st = new StreamTokenizer(new StringReader(description));
st.ordinaryChar('(');
st.ordinaryChar(')');
while (st.nextToken() != StreamTokenizer.TT_EOF) {
if (st.ttype == StreamTokenizer.TT_NUMBER) {
Integer thisInt = new Integer((int) st.nval);
if (thisInt < 1) {
log.error("number < 1");
return null;
}
tokens.add(thisInt);
} else if (st.ttype == '(') {
tokens.add("(");
} else if (st.ttype == ')') {
tokens.add(")");
} else if (st.ttype == StreamTokenizer.TT_WORD) {
String str = st.sval.toLowerCase();
tokens.add(str);
}
}
return createCompositionSearch(tokens);
}
catch (Exception ex) {
log.error("Error in description string: " + description, ex);
return null;
}
}
public static PatientSearch createCompositionSearch(Object[] tokens) {
return createCompositionSearch(Arrays.asList(tokens));
}
public static PatientSearch createCompositionSearch(List<Object> tokens) {
// TODO This is a rewrite of the code in CohortSearchHistory.createCompositionFilter(String). That method should probably delegate to this one in some way.
List<Object> currentLine = new ArrayList<Object>();
try {
Stack<List<Object>> stack = new Stack<List<Object>>();
for (Object token : tokens) {
if (token instanceof String) {
String s = (String) token;
s = s.toLowerCase();
if (andWords.contains(s)) {
currentLine.add(PatientSetService.BooleanOperator.AND);
} else if (orWords.contains(s)) {
currentLine.add(PatientSetService.BooleanOperator.OR);
} else if (notWords.contains(s)) {
currentLine.add(PatientSetService.BooleanOperator.NOT);
} else if (openParenthesesWords.contains(s)) {
stack.push(currentLine);
currentLine = new ArrayList<Object>();
} else if (closeParenthesesWords.contains(s)) {
List<Object> l = stack.pop();
l.add(currentLine);
currentLine = l;
} else {
throw new IllegalArgumentException("Unrecognized string token: " + s);
}
} else if (token instanceof Integer) {
currentLine.add(token);
} else if (token instanceof PatientSearch) {
currentLine.add(token);
} else if (token instanceof PatientFilter) {
currentLine.add(token);
} else {
throw new IllegalArgumentException("Unknown class in token list: " + token.getClass());
}
}
}
catch (Exception ex) {
log.error("Error in token list", ex);
return null;
}
PatientSearch ret = new PatientSearch();
ret.setParsedComposition(currentLine);
return ret;
}
@SuppressWarnings("unchecked")
public static PatientSearch createFilterSearch(Class filterClass) {
PatientSearch ps = new PatientSearch();
ps.setFilterClass(filterClass);
ps.setArguments(new ArrayList<SearchArgument>());
return ps;
}
// constructors and instance methods
public PatientSearch() {
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("PatientSearch");
if (getSavedCohortId() != null)
sb.append(" savedCohortId=" + getSavedCohortId());
if (getSavedFilterId() != null)
sb.append(" savedFilterId=" + getSavedFilterId());
if (getSavedSearchId() != null)
sb.append(" savedSearchId=" + getSavedSearchId());
if (getFilterClass() != null) {
sb.append(" filterClass=" + getFilterClass());
if (getArguments() != null)
for (SearchArgument sa : getArguments())
sb.append(" (" + sa.getPropertyClass() + ")" + sa.getName() + "=" + sa.getValue());
}
if (getParsedComposition() != null) {
sb.append(" parsedComposition=");
for (Object o : getParsedComposition())
sb.append("\n" + o);
}
if (parameterValues != null)
for (Map.Entry<String, String> e : parameterValues.entrySet())
sb.append(" paramValue:" + e.getKey() + "=" + e.getValue());
return sb.toString();
}
public boolean isComposition() {
return parsedComposition != null;
}
public String getCompositionString() {
if (parsedComposition == null)
return null;
else
return compositionStringHelper(parsedComposition);
}
/**
* Convenience method so that a PatientSearch object can be created from a string of
* compositions
*
* @param specification
*/
@Element(data = true, name = "specification", required = false)
public void setSpecificationString(String specification) {
PatientSearch temp = (PatientSearch) CohortUtil.parse(specification);
if (temp == null)
throw new IllegalArgumentException("Couldn't parse: " + specification);
this.setParsedComposition(temp.getParsedComposition());
this.setSavedSearchId(temp.getSavedSearchId());
this.setSavedFilterId(temp.getSavedFilterId());
this.setSavedCohortId(temp.getSavedCohortId());
this.setFilterClass(temp.getFilterClass());
if (temp.getArguments() != null)
this.setArguments(new ArrayList<SearchArgument>(temp.getArguments()));
else
this.setArguments(null);
}
@Element(data = true, name = "specification", required = false)
public String getSpecificationString() {
return "Not Yet Implemented";
}
@SuppressWarnings("unchecked")
private String compositionStringHelper(List list) {
StringBuilder ret = new StringBuilder();
for (Object o : list) {
if (ret.length() > 0)
ret.append(" ");
if (o instanceof List)
ret.append("(" + compositionStringHelper((List) o) + ")");
else
ret.append(o);
}
return ret.toString();
}
/**
* @return Whether this search requires a history against which to evaluate it
*/
public boolean requiresHistory() {
if (isComposition()) {
return requiresHistoryHelper(parsedComposition);
} else
return false;
}
private boolean requiresHistoryHelper(List<Object> list) {
for (Object o : list) {
if (o instanceof Integer)
return true;
else if (o instanceof PatientSearch)
return ((PatientSearch) o).requiresHistory();
else if (o instanceof List) {
if (requiresHistoryHelper((List<Object>) o))
return true;
}
}
return false;
}
/**
* Creates a copy of this PatientSearch that doesn't depend on history, replacing references
* with actual PatientSearch elements from the provided history. The PatientSearch object
* returned is only a copy when necessary to detach it from history. This method does NOT do a
* clone.
*/
public PatientSearch copyAndDetachFromHistory(CohortSearchHistory history) {
if (isComposition() && requiresHistory()) {
PatientSearch copy = new PatientSearch();
copy.setParsedComposition(copyAndDetachHelper(parsedComposition, history));
return copy;
} else
return this;
}
@SuppressWarnings("unchecked")
private List<Object> copyAndDetachHelper(List<Object> list, CohortSearchHistory history) {
List<Object> ret = new ArrayList<Object>();
for (Object o : list) {
if (o instanceof PatientSearch) {
ret.add(((PatientSearch) o).copyAndDetachFromHistory(history));
} else if (o instanceof Integer) {
PatientSearch ps = history.getSearchHistory().get(((Integer) o) - 1);
ret.add(ps.copyAndDetachFromHistory(history));
} else if (o instanceof List) {
ret.add(copyAndDetachHelper((List) o, history));
} else
ret.add(o);
}
return ret;
}
/**
* Deep-copies this.parsedComposition, and converts to filters, in the context of history
*/
public CohortHistoryCompositionFilter cloneCompositionAsFilter(CohortSearchHistory history) {
return cloneCompositionAsFilter(history, null);
}
/**
* Deep-copies this.parsedComposition, and converts to filters, in the context of history
*/
public CohortHistoryCompositionFilter cloneCompositionAsFilter(CohortSearchHistory history, EvaluationContext evalContext) {
List<Object> list = cloneCompositionHelper(parsedComposition, history, evalContext);
CohortHistoryCompositionFilter pf = new CohortHistoryCompositionFilter();
pf.setParsedCompositionString(list);
pf.setHistory(history);
return pf;
}
@SuppressWarnings("unchecked")
private List<Object> cloneCompositionHelper(List<Object> list, CohortSearchHistory history, EvaluationContext evalContext) {
List<Object> ret = new ArrayList<Object>();
for (Object o : list) {
if (o instanceof List)
ret.add(cloneCompositionHelper((List) o, history, evalContext));
else if (o instanceof Integer)
ret.add(history.ensureCachedFilter((Integer) o - 1));
else if (o instanceof BooleanOperator)
ret.add(o);
else if (o instanceof PatientFilter)
ret.add(o);
else if (o instanceof PatientSearch)
ret.add(OpenmrsUtil.toPatientFilter((PatientSearch) o, history, evalContext));
else
throw new RuntimeException("Programming Error: forgot to handle: " + o.getClass());
}
return ret;
}
public boolean isSavedReference() {
return isSavedSearchReference() || isSavedFilterReference() || isSavedCohortReference();
}
public boolean isSavedSearchReference() {
return savedSearchId != null;
}
public boolean isSavedFilterReference() {
return savedFilterId != null;
}
public boolean isSavedCohortReference() {
return savedCohortId != null;
}
/**
* Call this to notify this composition search that the _i_th element of the search history has
* been removed, and the search potentially needs to renumber its constituent parts. Examples,
* assuming this search is "1 and (4 or
* 5)": * removeFromHistoryNotify(1) -> This search becomes "1 and (3 or 4)" and the method
* return false * removeFromHistoryNotify(3) -> This search becomes invalid, and the method
* returns true * removeFromHistoryNotify(9) -> This search is unaffected, and the method
* returns false
*
* @return whether or not this search itself should be removed (because it directly references
* the removed history element
*/
public boolean removeFromHistoryNotify(int i) {
if (!isComposition())
throw new IllegalArgumentException("Can only call this method on a composition search");
return removeHelper(parsedComposition, i);
}
@SuppressWarnings("unchecked")
private boolean removeHelper(List<Object> list, int i) {
boolean ret = false;
for (ListIterator<Object> iter = list.listIterator(); iter.hasNext();) {
Object o = iter.next();
if (o instanceof List)
ret |= removeHelper((List<Object>) o, i);
else if (o instanceof Integer) {
Integer ref = (Integer) o;
if (ref == i) {
ret = true;
iter.set("-1");
} else if (ref > i)
iter.set(ref - 1);
}
}
return ret;
}
/**
* Looks up an argument value, accounting for parameterValues
*
* @param name
* @return the <code>String</code> value for the specified argument
*/
public String getArgumentValue(String name) {
if (parameterValues.containsKey(name))
return parameterValues.get(name);
for (SearchArgument sa : arguments)
if (sa.getName().equals(name))
return sa.getValue();
return null;
}
@ElementList(required = false)
public List<SearchArgument> getArguments() {
return arguments;
}
@ElementList(required = false)
public void setArguments(List<SearchArgument> arguments) {
this.arguments = arguments;
}
/**
* Returns all SearchArgument values that match
* {@link org.openmrs.report.EvaluationContext#parameterValues}
*
* @return <code>List<Parameter></code> of all parameters in the arguments
*/
public List<Parameter> getParameters() {
List<Parameter> parameters = new ArrayList<Parameter>();
if (arguments != null) {
for (SearchArgument a : arguments) {
String value = parameterValues.get(a.getName());
if (value == null)
value = a.getValue();
if (EvaluationContext.isExpression(value)) {
parameters.add(new Parameter(a.getName(), a.getName(), a.getPropertyClass(), value));
}
}
}
return parameters;
}
@SuppressWarnings("unchecked")
@Attribute(required = false)
public Class getFilterClass() {
return filterClass;
}
@SuppressWarnings("unchecked")
@Attribute(required = false)
public void setFilterClass(Class clazz) {
if (clazz != null && !PatientFilter.class.isAssignableFrom(clazz))
throw new IllegalArgumentException(clazz + " is not an org.openmrs.PatientFilter");
this.filterClass = clazz;
}
@SuppressWarnings("unchecked")
public void addArgument(String name, String value, Class clz) {
addArgument(new SearchArgument(name, value, clz));
}
public void addArgument(SearchArgument sa) {
if (arguments == null)
arguments = new ArrayList<SearchArgument>();
arguments.add(sa);
}
/**
* Adds a SearchArgument as a Parameter where the SearchArgument name is set to the Parameter
* label and SearchArgument value is set to the Parameter name and SearchArgument propertyClass
* is set to the Parameter clazz
*
* @param parameter
*/
public void addParameter(Parameter parameter) {
addArgument(parameter.getLabel(), parameter.getName(), parameter.getClazz());
}
//@ElementList(required=false)
public List<Object> getParsedComposition() {
return parsedComposition;
}
/**
* Elements in this list can be: an Integer, indicating a 1-based index into a search history a
* BooleanOperator (AND, OR, NOT) a PatientFilter a PatientSearch another List of the same form,
* which indicates a parenthetical expression
*/
//@ElementList(required=false)
public void setParsedComposition(List<Object> parsedComposition) {
this.parsedComposition = parsedComposition;
}
@Attribute(required = false)
public Integer getSavedSearchId() {
return savedSearchId;
}
@Attribute(required = false)
public void setSavedSearchId(Integer savedSearchId) {
this.savedSearchId = savedSearchId;
}
@Attribute(required = false)
public Integer getSavedFilterId() {
return savedFilterId;
}
@Attribute(required = false)
public void setSavedFilterId(Integer savedFilterId) {
this.savedFilterId = savedFilterId;
}
@Attribute(required = false)
public Integer getSavedCohortId() {
return savedCohortId;
}
@Attribute(required = false)
public void setSavedCohortId(Integer savedCohortId) {
this.savedCohortId = savedCohortId;
}
public void setParameterValue(String name, String value) {
parameterValues.put(name, value);
}
}