/** * 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.cohort; import java.io.StreamTokenizer; import java.io.StringReader; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.Cohort; import org.openmrs.api.PatientSetService; import org.openmrs.api.PatientSetService.BooleanOperator; import org.openmrs.api.context.Context; import org.openmrs.report.EvaluationContext; import org.openmrs.reporting.AbstractReportObject; import org.openmrs.reporting.PatientFilter; import org.openmrs.reporting.PatientSearch; import org.openmrs.reporting.ReportObject; import org.openmrs.util.OpenmrsUtil; /** * @deprecated see reportingcompatibility module */ @Deprecated public class CohortSearchHistory extends AbstractReportObject { protected transient final Log log = LogFactory.getLog(getClass()); public class CohortSearchHistoryItemHolder { private PatientSearch search; private PatientFilter filter; private String name; private String description; private Boolean saved; private Cohort cachedResult; private Date cachedResultDate; public CohortSearchHistoryItemHolder() { } public Cohort getCachedResult() { return cachedResult; } public void setCachedResult(Cohort cachedResult) { this.cachedResult = cachedResult; } public Date getCachedResultDate() { return cachedResultDate; } public void setCachedResultDate(Date cachedResultDate) { this.cachedResultDate = cachedResultDate; } public PatientSearch getSearch() { return search; } public void setSearch(PatientSearch search) { this.search = search; } public PatientFilter getFilter() { return filter; } public void setFilter(PatientFilter filter) { this.filter = filter; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Boolean getSaved() { return saved; } public void setSaved(Boolean saved) { this.saved = saved; } } private List<PatientSearch> searchHistory; private volatile List<PatientFilter> cachedFilters; private volatile List<Cohort> cachedResults; private volatile List<Date> cachedResultDates; public CohortSearchHistory() { super.setType("Search History"); super.setSubType("Search History"); searchHistory = new ArrayList<PatientSearch>(); cachedFilters = new ArrayList<PatientFilter>(); cachedResults = new ArrayList<Cohort>(); cachedResultDates = new ArrayList<Date>(); } public synchronized List<CohortSearchHistoryItemHolder> getItems() { checkArrayLengths(); List<CohortSearchHistoryItemHolder> ret = new ArrayList<CohortSearchHistoryItemHolder>(); for (int i = 0; i < searchHistory.size(); ++i) { CohortSearchHistoryItemHolder item = new CohortSearchHistoryItemHolder(); PatientSearch search = searchHistory.get(i); item.setSearch(search); ensureCachedFilter(i); PatientFilter filter = cachedFilters.get(i); item.setFilter(filter); if (search.isSavedFilterReference()) { ReportObject ro = Context.getReportObjectService().getReportObject(search.getSavedFilterId()); item.setName(ro.getName()); item.setDescription(ro.getDescription()); } else if (search.isSavedCohortReference()) { org.openmrs.Cohort c = Context.getCohortService().getCohort(search.getSavedCohortId()); item.setName(c.getName()); item.setDescription(c.getDescription()); } else if (search.isSavedSearchReference()) { ReportObject ro = Context.getReportObjectService().getReportObject(search.getSavedSearchId()); item.setName(ro.getName()); item.setDescription(ro.getDescription()); } else if (search.isComposition()) { item.setName(search.getCompositionString()); } else { item.setName(filter.getName()); item.setDescription(filter.getDescription()); } item.setSaved(search.isSavedReference()); item.setCachedResult(cachedResults.get(i)); item.setCachedResultDate(cachedResultDates.get(i)); ret.add(item); } return ret; } public List<PatientSearch> getSearchHistory() { return searchHistory; } public void setSearchHistory(List<PatientSearch> searchHistory) { this.searchHistory = searchHistory; cachedFilters = new ArrayList<PatientFilter>(); cachedResults = new ArrayList<Cohort>(); cachedResultDates = new ArrayList<Date>(); for (int i = 0; i < searchHistory.size(); ++i) { cachedFilters.add(null); cachedResults.add(null); cachedResultDates.add(null); } } public List<PatientFilter> getCachedFilters() { return cachedFilters; } public List<Date> getCachedResultDates() { return cachedResultDates; } public List<Cohort> getCachedResults() { return cachedResults; } public int size() { return searchHistory.size(); } public int getSize() { return size(); } public synchronized void addSearchItem(PatientSearch ps) { checkArrayLengths(); searchHistory.add(ps); cachedFilters.add(null); cachedResults.add(null); cachedResultDates.add(null); } public synchronized void removeSearchItem(int i) { checkArrayLengths(); List<Integer> toDelete = new ArrayList<Integer>(); toDelete.add(i); while (toDelete.size() > 0) { int index = toDelete.remove(0); List<Integer> toCascade = removeSearchItemHelper(index); searchHistory.remove(index); cachedFilters.remove(index); cachedResults.remove(index); cachedResultDates.remove(index); toDelete.addAll(toCascade); } } /** * @return zero-based indices that should also be removed due to cascading. (These will already * have had 1 subtracted from them, since we know that a search from above is being * deleted) */ private synchronized List<Integer> removeSearchItemHelper(int i) { // 1. Decrement any number in a CohortHistoryCompositionFilter that's greater than i. // 2. If any CohortHistoryCompositionFilter references search i, we'll have to cascade delete it List<Integer> ret = new ArrayList<Integer>(); for (int j = i + 1; j < searchHistory.size(); ++j) { PatientSearch ps = searchHistory.get(j); if (ps.isComposition()) { cachedFilters.set(i, null); // this actually only needs to happen if the filter is affected // note that i is zero-based, but in a composition filter it would be one-based boolean removeMeToo = ps.removeFromHistoryNotify(i + 1); if (removeMeToo) ret.add(j - 1); } } return ret; } public synchronized PatientFilter ensureCachedFilter(int i) { if (cachedFilters.get(i) == null) cachedFilters.set(i, OpenmrsUtil.toPatientFilter(searchHistory.get(i), this)); return cachedFilters.get(i); } /** * @param i * @return patient set resulting from the i_th filter in the search history. (cached if * possible) */ public Cohort getPatientSet(int i, EvaluationContext context) { return getPatientSet(i, true, context); } /** * TODO: Implement {@link org.openmrs.api.impl.CohortServiceImpl#getAllCohorts()} * * @param i * @param useCache whether to use a cached result, if available * @return patient set resulting from the i_th filter in the search history */ public Cohort getPatientSet(int i, boolean useCache, EvaluationContext context) { checkArrayLengths(); Cohort ret = null; if (useCache) { ret = cachedResults.get(i); } if (ret == null) { ensureCachedFilter(i); PatientFilter pf = cachedFilters.get(i); Cohort everyone = Context.getPatientSetService().getAllPatients(); ret = pf.filter(everyone, context); cachedFilters.set(i, pf); cachedResults.set(i, ret); cachedResultDates.set(i, new Date()); } return ret; } public Cohort getLastPatientSet(EvaluationContext context) { if (searchHistory.size() > 0) return getPatientSet(searchHistory.size() - 1, context); else return new Cohort(); } public Cohort getPatientSetCombineWithAnd(EvaluationContext context) { Set<Integer> current = null; for (int i = 0; i < searchHistory.size(); ++i) { Cohort ps = getPatientSet(i, context); if (current == null) current = new HashSet<Integer>(ps.getMemberIds()); else current.retainAll(ps.getMemberIds()); } if (current == null) return Context.getPatientSetService().getAllPatients(); else { return new Cohort("Cohort anded together", "", current); } } public Cohort getPatientSetCombineWithOr(EvaluationContext context) { Set<Integer> ret = new HashSet<Integer>(); for (int i = 0; i < searchHistory.size(); ++i) { ret.addAll(getPatientSet(i, context).getMemberIds()); } return new Cohort("Cohort or'd together", "", ret); } // Just in case someone has modified the searchHistory list directly. Maybe I should make that getter return an unmodifiable list. // TODO: this isn't actually good enough. Use the unmodifiable list method instead private synchronized void checkArrayLengths() { int n = searchHistory.size(); while (cachedFilters.size() > n) cachedFilters.remove(n); while (cachedResults.size() > n) cachedResults.remove(n); while (cachedResultDates.size() > n) cachedResultDates.remove(n); while (cachedFilters.size() < n) cachedFilters.add(null); while (cachedResults.size() < n) cachedResults.add(null); while (cachedResultDates.size() < n) cachedResultDates.add(null); } public PatientSearch createCompositionFilter(String description) { Set<String> andWords = new HashSet<String>(); Set<String> orWords = new HashSet<String>(); Set<String> notWords = new HashSet<String>(); andWords.add("and"); andWords.add("intersection"); andWords.add("*"); orWords.add("or"); orWords.add("union"); orWords.add("+"); notWords.add("not"); notWords.add("!"); List<Object> currentLine = new ArrayList<Object>(); try { StreamTokenizer st = new StreamTokenizer(new StringReader(description)); st.ordinaryChar('('); st.ordinaryChar(')'); Stack<List<Object>> stack = new Stack<List<Object>>(); while (st.nextToken() != StreamTokenizer.TT_EOF) { if (st.ttype == StreamTokenizer.TT_NUMBER) { Integer thisInt = new Integer((int) st.nval); if (thisInt < 1 || thisInt > searchHistory.size()) { log.error("number < 1 or > search history size"); return null; } currentLine.add(thisInt); } else if (st.ttype == '(') { stack.push(currentLine); currentLine = new ArrayList<Object>(); } else if (st.ttype == ')') { List<Object> l = stack.pop(); l.add(currentLine); currentLine = l; } else if (st.ttype == StreamTokenizer.TT_WORD) { String str = st.sval.toLowerCase(); if (andWords.contains(str)) currentLine.add(PatientSetService.BooleanOperator.AND); else if (orWords.contains(str)) currentLine.add(PatientSetService.BooleanOperator.OR); else if (notWords.contains(str)) currentLine.add(PatientSetService.BooleanOperator.NOT); else throw new IllegalArgumentException("Don't recognize " + st.sval); } } } catch (Exception ex) { log.error("Error in description string: " + description, ex); return null; } if (!testCompositionList(currentLine)) { log.error("Description string failed test: " + description); return null; } //return toPatientFilter(currentLine); PatientSearch ret = new PatientSearch(); ret.setParsedComposition(currentLine); return ret; } @SuppressWarnings("unchecked") private static boolean testCompositionList(List<Object> list) { // if length > 2, make sure there's at least one operator // make sure NOT is always followed by something // make sure not everything is a logical operator // can't have two logical operators in a row (unless the second is a NOT) boolean anyNonOperator = false; boolean anyOperator = false; boolean lastIsNot = false; boolean lastIsOperator = false; boolean childrenOkay = true; for (Object o : list) { if (o instanceof List) { childrenOkay &= testCompositionList((List<Object>) o); anyNonOperator = true; } else if (o instanceof BooleanOperator) { if (lastIsOperator && (BooleanOperator) o != BooleanOperator.NOT) return false; anyOperator = true; } else if (o instanceof Integer) { anyNonOperator = true; } else { throw new RuntimeException("Programming error! unexpected class " + o.getClass()); } lastIsNot = ((o instanceof BooleanOperator) && (((BooleanOperator) o) == BooleanOperator.NOT)); lastIsOperator = o instanceof BooleanOperator; } if (list.size() > 2 && !anyOperator) return false; if (lastIsNot) return false; if (!anyNonOperator) return false; return true; } }