HibernatePatientSetDAO.java

/**
 * 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.api.db.hibernate;

import java.io.StringWriter;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.CacheMode;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.StringType;
import org.openmrs.Cohort;
import org.openmrs.Concept;
import org.openmrs.Drug;
import org.openmrs.DrugOrder;
import org.openmrs.Encounter;
import org.openmrs.EncounterType;
import org.openmrs.Form;
import org.openmrs.Location;
import org.openmrs.Obs;
import org.openmrs.Order;
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientIdentifierType;
import org.openmrs.PatientProgram;
import org.openmrs.PatientState;
import org.openmrs.Person;
import org.openmrs.PersonAttributeType;
import org.openmrs.PersonName;
import org.openmrs.Program;
import org.openmrs.ProgramWorkflow;
import org.openmrs.ProgramWorkflowState;
import org.openmrs.Relationship;
import org.openmrs.RelationshipType;
import org.openmrs.User;
import org.openmrs.api.EncounterService;
import org.openmrs.api.ObsService;
import org.openmrs.api.PatientService;
import org.openmrs.api.PatientSetService;
import org.openmrs.api.PatientSetService.Modifier;
import org.openmrs.api.PatientSetService.PatientLocationMethod;
import org.openmrs.api.PatientSetService.TimeModifier;
import org.openmrs.api.context.Context;
import org.openmrs.api.db.DAOException;
import org.openmrs.api.db.PatientSetDAO;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Hibernate specific implementation of the PatientSetDAO. <br/>
 * <br/>
 * This class should not be instantiated. Rather, it is injected into the PatientSetService by
 * Spring.
 * 
 * @see org.openmrs.api.context.Context
 * @see org.openmrs.api.PatientSetService
 * @see org.openmrs.api.db.PatientSetDAO
 */
public class HibernatePatientSetDAO implements PatientSetDAO {
	
	protected final Log log = LogFactory.getLog(getClass());
	
	/**
	 * Hibernate sessionFactory.getCurrentSession() factory
	 */
	private SessionFactory sessionFactory;
	
	/**
	 * Set sessionFactory.getCurrentSession() factory
	 * 
	 * @param sessionFactory SessionFactory to set
	 */
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
	
	/**
	 * @deprecated
	 * @see org.openmrs.api.db.PatientSetDAO#exportXml(org.openmrs.Cohort)
	 */
	public String exportXml(Cohort ps) throws DAOException {
		StringBuffer ret = new StringBuffer("<patientset>");
		for (Integer patientId : ps.getMemberIds()) {
			ret.append(exportXml(patientId));
		}
		ret.append("</patientset>");
		return ret.toString();
	}
	
	private String formatUserName(User u) {
		return u.getPersonName().getFullName();
	}
	
	private String formatUser(User u) {
		StringBuilder ret = new StringBuilder();
		ret.append(u.getUserId() + "^" + formatUserName(u));
		return ret.toString();
	}
	
	DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	private Element obsElementHelper(Document doc, Locale locale, Obs obs) {
		Element obsNode = doc.createElement("obs");
		Concept c = obs.getConcept();
		
		obsNode.setAttribute("obs_id", obs.getObsId().toString());
		obsNode.setAttribute("concept_id", c.getConceptId().toString());
		obsNode.setAttribute("concept_name", c.getName(locale).getName());
		
		if (obs.getObsDatetime() != null) {
			obsNode.setAttribute("datetime", df.format(obs.getObsDatetime()));
		}
		if (obs.getAccessionNumber() != null) {
			obsNode.setAttribute("accession_number", obs.getAccessionNumber());
		}
		if (obs.getComment() != null) {
			obsNode.setAttribute("comment", obs.getComment());
		}
		if (obs.getObsGroup() != null) {
			obsNode.setAttribute("obs_group_id", obs.getObsGroup().getObsId().toString());
		}
		if (obs.getValueGroupId() != null) {
			obsNode.setAttribute("value_group_id", obs.getValueGroupId().toString());
		}
		
		String value = null;
		String dataType = null;
		
		if (obs.getValueCoded() != null) {
			Concept valueConcept = obs.getValueCoded();
			obsNode.setAttribute("value_coded_id", valueConcept.getConceptId().toString());
			obsNode.setAttribute("value_coded", valueConcept.getName(locale).getName());
			dataType = "coded";
			value = valueConcept.getName(locale).getName();
		}
		if (obs.getValueAsBoolean() != null) {
			obsNode.setAttribute("value_boolean", obs.getValueAsBoolean().toString());
			dataType = "boolean";
			value = obs.getValueAsBoolean().toString();
		}
		if (obs.getValueDatetime() != null) {
			obsNode.setAttribute("value_datetime", df.format(obs.getValueDatetime()));
			dataType = "datetime";
			value = obs.getValueDatetime().toString();
		}
		if (obs.getValueNumeric() != null) {
			obsNode.setAttribute("value_numeric", obs.getValueNumeric().toString());
			dataType = "numeric";
			value = obs.getValueNumeric().toString();
		}
		if (obs.getValueText() != null) {
			obsNode.setAttribute("value_text", obs.getValueText());
			dataType = "text";
			value = obs.getValueText();
		}
		if (obs.getValueModifier() != null) {
			obsNode.setAttribute("value_modifier", obs.getValueModifier());
			if (value != null) {
				value = obs.getValueModifier() + " " + value;
			}
		}
		obsNode.setAttribute("data_type", dataType);
		obsNode.appendChild(doc.createTextNode(value));
		
		return obsNode;
	}
	
	/**
	 * Note that the formatting may depend on locale
	 * 
	 * @deprecated
	 */
	@Deprecated
	public String exportXml(Integer patientId) throws DAOException {
		Locale locale = Context.getLocale();
		
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		Document doc = null;
		
		PatientService patientService = Context.getPatientService();
		EncounterService encounterService = Context.getEncounterService();
		
		Patient p = patientService.getPatient(patientId);
		List<Encounter> encounters = encounterService.getEncountersByPatientId(patientId);
		
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.newDocument();
			
			Element root = (Element) doc.createElement("patient_data");
			doc.appendChild(root);
			
			Element patientNode = doc.createElement("patient");
			patientNode.setAttribute("patient_id", p.getPatientId().toString());
			
			boolean firstName = true;
			Element namesNode = doc.createElement("names");
			for (PersonName name : p.getNames()) {
				if (firstName) {
					if (name.getGivenName() != null) {
						patientNode.setAttribute("given_name", name.getGivenName());
					}
					if (name.getMiddleName() != null) {
						patientNode.setAttribute("middle_name", name.getMiddleName());
					}
					if (name.getFamilyName() != null) {
						patientNode.setAttribute("family_name", name.getFamilyName());
					}
					if (name.getFamilyName2() != null) {
						patientNode.setAttribute("family_name2", name.getFamilyName2());
					}
					firstName = false;
				}
				Element nameNode = doc.createElement("name");
				if (name.getGivenName() != null) {
					nameNode.setAttribute("given_name", name.getGivenName());
				}
				if (name.getMiddleName() != null) {
					nameNode.setAttribute("middle_name", name.getMiddleName());
				}
				if (name.getFamilyName() != null) {
					nameNode.setAttribute("family_name", name.getFamilyName());
				}
				if (name.getFamilyName2() != null) {
					nameNode.setAttribute("family_name2", name.getFamilyName2());
				}
				namesNode.appendChild(nameNode);
			}
			patientNode.appendChild(namesNode);
			patientNode.setAttribute("gender", p.getGender());
			
			/*
			if (p.getRace() != null) {
				patientNode.setAttribute("race", p.getRace());
			}
			 */
			if (p.getBirthdate() != null) {
				patientNode.setAttribute("birthdate", df.format(p.getBirthdate()));
			}
			if (p.getBirthdateEstimated() != null) {
				patientNode.setAttribute("birthdate_estimated", p.getBirthdateEstimated().toString());
			}
			/*
			if (p.getBirthplace() != null) {
				patientNode.setAttribute("birthplace", p.getBirthplace());
			}
			*/
			/*
			if (p.getCitizenship() != null) {
				patientNode.setAttribute("citizenship", p.getCitizenship());
			}
			 */
			/*
			if (p.getMothersName() != null) {
				patientNode.setAttribute("mothers_name", p.getMothersName());
			}
			if (p.getCivilStatus() != null) {
				patientNode.setAttribute("civil_status", p.getCivilStatus().getName(locale, false).getName());
			}
			 */
			if (p.getDeathDate() != null) {
				patientNode.setAttribute("death_date", df.format(p.getDeathDate()));
			}
			if (p.getDeathdateEstimated() != null) {
				patientNode.setAttribute("deathdate_estimated", p.getDeathdateEstimated().toString());
			}
			
			if (p.getCauseOfDeath() != null) {
				patientNode.setAttribute("cause_of_death", p.getCauseOfDeath().getName(locale, false).getName());
			}
			/*
			if (p.getHealthDistrict() != null) {
				patientNode.setAttribute("health_district", p.getHealthDistrict());
			}
			if (p.getHealthCenter() != null) {
				patientNode.setAttribute("health_center", p.getHealthCenter().getName());
				patientNode.setAttribute("health_center_id", p.getHealthCenter().getLocationId().toString());
			}
			 */

			for (Encounter e : encounters) {
				Element encounterNode = doc.createElement("encounter");
				if (e.getEncounterDatetime() != null) {
					encounterNode.setAttribute("datetime", df.format(e.getEncounterDatetime()));
				}
				
				Element metadataNode = doc.createElement("metadata");
				{
					Location l = e.getLocation();
					if (l != null) {
						Element temp = doc.createElement("location");
						temp.setAttribute("location_id", l.getLocationId().toString());
						temp.appendChild(doc.createTextNode(l.getName()));
						metadataNode.appendChild(temp);
					}
					EncounterType t = e.getEncounterType();
					if (t != null) {
						Element temp = doc.createElement("encounter_type");
						temp.setAttribute("encounter_type_id", t.getEncounterTypeId().toString());
						temp.appendChild(doc.createTextNode(t.getName()));
						metadataNode.appendChild(temp);
					}
					Form f = e.getForm();
					if (f != null) {
						Element temp = doc.createElement("form");
						temp.setAttribute("form_id", f.getFormId().toString());
						temp.appendChild(doc.createTextNode(f.getName()));
						metadataNode.appendChild(temp);
					}
					Person u = e.getProvider();
					if (u != null) {
						Element temp = doc.createElement("provider");
						temp.setAttribute("provider_id", u.getPersonId().toString());
						temp.appendChild(doc.createTextNode(u.getPersonName().getFullName()));
						metadataNode.appendChild(temp);
					}
				}
				encounterNode.appendChild(metadataNode);
				
				Collection<Obs> observations = e.getObs();
				if (observations != null && observations.size() > 0) {
					Element observationsNode = doc.createElement("observations");
					for (Obs obs : observations) {
						Element obsNode = obsElementHelper(doc, locale, obs);
						observationsNode.appendChild(obsNode);
					}
					encounterNode.appendChild(observationsNode);
				}
				
				Set<Order> orders = e.getOrders();
				if (orders != null && orders.size() != 0) {
					Element ordersNode = doc.createElement("orders");
					for (Order order : orders) {
						Element orderNode = doc.createElement("order");
						orderNode.setAttribute("order_id", order.getOrderId().toString());
						
						Concept concept = order.getConcept();
						orderNode.setAttribute("concept_id", concept.getConceptId().toString());
						orderNode.appendChild(doc.createTextNode(concept.getName(locale).getName()));
						
						if (order.getInstructions() != null) {
							orderNode.setAttribute("instructions", order.getInstructions());
						}
						if (order.getStartDate() != null) {
							orderNode.setAttribute("start_date", df.format(order.getStartDate()));
						}
						if (order.getAutoExpireDate() != null) {
							orderNode.setAttribute("auto_expire_date", df.format(order.getAutoExpireDate()));
						}
						if (order.getOrderer() != null) {
							orderNode.setAttribute("orderer", order.getOrderer().getProviderId() + "^"
							        + order.getOrderer().getName());
						}
						if (order.getDiscontinued() != null) {
							orderNode.setAttribute("discontinued", order.getDiscontinued().toString());
						}
						if (order.getDiscontinuedDate() != null) {
							orderNode.setAttribute("discontinued_date", df.format(order.getDiscontinuedDate()));
						}
						if (order.getDiscontinuedReason() != null) {
							orderNode.setAttribute("discontinued_reason", order.getDiscontinuedReason());
						}
						
						ordersNode.appendChild(orderNode);
					}
				}
				
				patientNode.appendChild(encounterNode);
			}
			
			ObsService obsService = Context.getObsService();
			List<Obs> allObservations = obsService.getObservationsByPerson(p);
			if (allObservations != null && allObservations.size() > 0) {
				log.debug("allObservations has " + allObservations.size() + " obs");
				Set<Obs> undoneObservations = new HashSet<Obs>();
				for (Obs obs : allObservations) {
					if (obs.getEncounter() == null) {
						undoneObservations.add(obs);
					}
				}
				log.debug("undoneObservations has " + undoneObservations.size() + " obs");
				
				if (undoneObservations.size() > 0) {
					Element observationsNode = doc.createElement("observations");
					for (Obs obs : undoneObservations) {
						Element obsNode = obsElementHelper(doc, locale, obs);
						observationsNode.appendChild(obsNode);
						log.debug("added node " + obsNode + " to observationsNode");
					}
					patientNode.appendChild(observationsNode);
				}
			}
			
			root.appendChild(patientNode);
			
		}
		catch (Exception ex) {
			throw new DAOException(ex);
		}
		
		String ret = null;
		
		try {
			Source source = new DOMSource(doc);
			StringWriter sw = new StringWriter();
			Result result = new StreamResult(sw);
			
			Transformer xformer = TransformerFactory.newInstance().newTransformer();
			xformer.transform(source, result);
			ret = sw.toString();
		}
		catch (Exception ex) {
			throw new DAOException(ex);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Cohort getAllPatients() {
		
		Query query = sessionFactory.getCurrentSession().createQuery("select patientId from Patient p where p.voided = '0'");
		
		Set<Integer> ids = new HashSet<Integer>();
		ids.addAll(query.list());
		
		return new Cohort("All patients", "", ids);
	}
	
	/**
	 * given program, workflow, and state, within a given date range
	 * 
	 * @param program The program the patient must have been in
	 * @param stateList List of states the patient must have been in (implies a workflow) (can be
	 *            null)
	 * @param fromDate If not null, then only patients in the given program/workflow/state on or
	 *            after this date
	 * @param toDate If not null, then only patients in the given program/workflow/state on or
	 *            before this date
	 * @return Cohort of Patients matching criteria
	 */
	public Cohort getPatientsByProgramAndState(Program program, List<ProgramWorkflowState> stateList, Date fromDate,
	        Date toDate) {
		Integer programId = program == null ? null : program.getProgramId();
		List<Integer> stateIds = null;
		if (stateList != null && stateList.size() > 0) {
			stateIds = new ArrayList<Integer>();
			for (ProgramWorkflowState state : stateList)
				stateIds.add(state.getProgramWorkflowStateId());
		}
		
		List<String> clauses = new ArrayList<String>();
		clauses.add("pp.voided = false");
		if (programId != null)
			clauses.add("pp.program_id = :programId");
		if (stateIds != null) {
			clauses.add("ps.state in (:stateIds)");
			clauses.add("ps.voided = false");
		}
		if (fromDate != null) {
			clauses.add("(pp.date_completed is null or pp.date_completed >= :fromDate)");
			if (stateIds != null)
				clauses.add("(ps.end_date is null or ps.end_date >= :fromDate)");
		}
		if (toDate != null) {
			clauses.add("(pp.date_enrolled is null or pp.date_enrolled <= :toDate)");
			if (stateIds != null)
				clauses.add("(ps.start_date is null or ps.start_date <= :toDate)");
		}
		
		StringBuilder sql = new StringBuilder();
		sql.append("select pp.patient_id ");
		sql.append("from patient_program pp ");
		sql.append("inner join patient p on pp.patient_id = p.patient_id and p.voided = false ");
		if (stateIds != null)
			sql.append("inner join patient_state ps on pp.patient_program_id = ps.patient_program_id ");
		for (ListIterator<String> i = clauses.listIterator(); i.hasNext();) {
			sql.append(i.nextIndex() == 0 ? " where " : " and ");
			sql.append(i.next());
		}
		sql.append(" group by pp.patient_id");
		log.debug("query: " + sql);
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sql.toString());
		if (programId != null)
			query.setInteger("programId", programId);
		if (stateIds != null)
			query.setParameterList("stateIds", stateIds);
		if (fromDate != null)
			query.setDate("fromDate", fromDate);
		if (toDate != null)
			query.setDate("toDate", toDate);
		
		return new Cohort(query.list());
	}
	
	/**
	 * a given program. If fromDate != null, then only those patients who were in the program at any
	 * time after that date if toDate != null, then only those patients who were in the program at
	 * any time before that date
	 */
	public Cohort getPatientsInProgram(Integer programId, Date fromDate, Date toDate) {
		String sql = "select pp.patient_id from patient_program pp ";
		sql += " inner join patient p on pp.patient_id = p.patient_id and p.voided = false ";
		sql += " where pp.voided = false and pp.program_id = :programId ";
		if (fromDate != null)
			sql += " and (date_completed is null or date_completed >= :fromDate) ";
		if (toDate != null)
			sql += " and (date_enrolled is null or date_enrolled <= :toDate) ";
		log.debug("sql: " + sql);
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sql);
		query.setCacheMode(CacheMode.IGNORE);
		
		query.setInteger("programId", programId);
		if (fromDate != null)
			query.setDate("fromDate", fromDate);
		if (toDate != null)
			query.setDate("toDate", toDate);
		
		return new Cohort(query.list());
	}
	
	public Cohort getPatientsHavingObs(Integer conceptId, PatientSetService.TimeModifier timeModifier,
	        PatientSetService.Modifier modifier, Object value, Date fromDate, Date toDate) {
		if (conceptId == null && value == null)
			throw new IllegalArgumentException("Can't have conceptId == null and value == null");
		if (conceptId == null && (timeModifier != TimeModifier.ANY && timeModifier != TimeModifier.NO))
			throw new IllegalArgumentException("If conceptId == null, timeModifier must be ANY or NO");
		if (conceptId == null && modifier != Modifier.EQUAL) {
			throw new IllegalArgumentException("If conceptId == null, modifier must be EQUAL");
		}
		Concept concept = null;
		if (conceptId != null)
			concept = Context.getConceptService().getConcept(conceptId);
		Number numericValue = null;
		String stringValue = null;
		Concept codedValue = null;
		Date dateValue = null;
		String valueSql = null;
		if (value != null) {
			if (concept == null) {
				if (value instanceof Concept)
					codedValue = (Concept) value;
				else
					codedValue = Context.getConceptService().getConceptByName(value.toString());
				valueSql = "o.value_coded";
			} else if (concept.getDatatype().isNumeric()) {
				if (value instanceof Number)
					numericValue = (Number) value;
				else
					numericValue = new Double(value.toString());
				valueSql = "o.value_numeric";
			} else if (concept.getDatatype().isText()) {
				stringValue = value.toString();
				valueSql = "o.value_text";
				if (modifier == null)
					modifier = Modifier.EQUAL;
			} else if (concept.getDatatype().isCoded()) {
				if (value instanceof Concept)
					codedValue = (Concept) value;
				else
					codedValue = Context.getConceptService().getConceptByName(value.toString());
				valueSql = "o.value_coded";
			} else if (concept.getDatatype().isDate()) {
				if (value instanceof Date) {
					dateValue = (Date) value;
				} else {
					try {
						dateValue = Context.getDateFormat().parse(value.toString());
					}
					catch (ParseException ex) {
						throw new IllegalArgumentException("Cannot interpret " + dateValue + " as a date in the format "
						        + Context.getDateFormat());
					}
				}
				valueSql = "o.value_datetime";
			} else if (concept.getDatatype().isBoolean()) {
				if (value instanceof Concept) {
					codedValue = (Concept) value;
				} else {
					boolean asBoolean = false;
					if (value instanceof Boolean)
						asBoolean = ((Boolean) value).booleanValue();
					else
						asBoolean = Boolean.valueOf(value.toString());
					codedValue = asBoolean ? Context.getConceptService().getTrueConcept() : Context.getConceptService()
					        .getFalseConcept();
				}
				valueSql = "o.value_coded";
			}
		}
		
		StringBuilder sb = new StringBuilder();
		boolean useValue = value != null;
		boolean doSqlAggregation = timeModifier == TimeModifier.MIN || timeModifier == TimeModifier.MAX
		        || timeModifier == TimeModifier.AVG;
		boolean doInvert = false;
		
		String dateSql = "";
		String dateSqlForSubquery = "";
		if (fromDate != null) {
			dateSql += " and o.obs_datetime >= :fromDate ";
			dateSqlForSubquery += " and obs_datetime >= :fromDate ";
		}
		if (toDate != null) {
			dateSql += " and o.obs_datetime <= :toDate ";
			dateSqlForSubquery += " and obs_datetime <= :toDate ";
		}
		
		if (timeModifier == TimeModifier.ANY || timeModifier == TimeModifier.NO) {
			if (timeModifier == TimeModifier.NO)
				doInvert = true;
			sb
			        .append("select o.person_id from obs o "
			                + "inner join patient p on o.person_id = p.patient_id and p.voided = false "
			                + "where o.voided = false ");
			if (conceptId != null)
				sb.append("and concept_id = :concept_id ");
			sb.append(dateSql);
			
		} else if (timeModifier == TimeModifier.FIRST || timeModifier == TimeModifier.LAST) {
			boolean isFirst = timeModifier == PatientSetService.TimeModifier.FIRST;
			sb.append("select o.person_id " + "from obs o inner join (" + "    select person_id, "
			        + (isFirst ? "min" : "max") + "(obs_datetime) as obs_datetime" + "    from obs"
			        + "    where voided = false and concept_id = :concept_id " + dateSqlForSubquery
			        + "    group by person_id"
			        + ") subq on o.person_id = subq.person_id and o.obs_datetime = subq.obs_datetime "
			        + " inner join patient p on o.person_id = p.patient_id and p.voided = false "
			        + "where o.voided = false and o.concept_id = :concept_id ");
			
		} else if (doSqlAggregation) {
			String sqlAggregator = timeModifier.toString();
			valueSql = sqlAggregator + "(" + valueSql + ")";
			sb.append("select o.person_id " + "from obs o "
			        + "inner join patient p on o.person_id = p.patient_id and p.voided = false "
			        + "where o.voided = false and concept_id = :concept_id " + dateSql + "group by o.person_id ");
			
		} else {
			throw new IllegalArgumentException("TimeModifier '" + timeModifier + "' not recognized");
		}
		
		if (useValue) {
			sb.append(doSqlAggregation ? " having " : " and ");
			sb.append(valueSql + " ");
			sb.append(modifier.getSqlRepresentation() + " :value");
		}
		if (!doSqlAggregation)
			sb.append(" group by o.person_id ");
		
		log.debug("query: " + sb);
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		query.setCacheMode(CacheMode.IGNORE);
		
		if (conceptId != null)
			query.setInteger("concept_id", conceptId);
		if (useValue) {
			if (numericValue != null)
				query.setDouble("value", numericValue.doubleValue());
			else if (codedValue != null)
				query.setInteger("value", codedValue.getConceptId());
			else if (stringValue != null)
				query.setString("value", stringValue);
			else if (dateValue != null)
				query.setDate("value", dateValue);
			else
				throw new IllegalArgumentException(
				        "useValue is true, but numeric, coded, string, boolean, and date values are all null");
		}
		if (fromDate != null)
			query.setDate("fromDate", fromDate);
		if (toDate != null)
			query.setDate("toDate", toDate);
		
		Cohort ret;
		if (doInvert) {
			ret = getAllPatients();
			ret.getMemberIds().removeAll(query.list());
		} else {
			ret = new Cohort(query.list());
		}
		
		return ret;
	}
	
	/**
	 * <pre>
	 * Returns the set of patients that have encounters, with several optional parameters:
	 *   of type encounterType
	 *   at a given location
	 *   from filling out a specific form
	 *   on or after fromDate
	 *   on or before toDate
	 *   patients with at least minCount of the given encounters
	 *   patients with up to maxCount of the given encounters
	 * </pre>
	 */
	public Cohort getPatientsHavingEncounters(List<EncounterType> encounterTypeList, Location location, Form form,
	        Date fromDate, Date toDate, Integer minCount, Integer maxCount) {
		List<Integer> encTypeIds = null;
		if (encounterTypeList != null && encounterTypeList.size() > 0) {
			encTypeIds = new ArrayList<Integer>();
			for (EncounterType t : encounterTypeList)
				encTypeIds.add(t.getEncounterTypeId());
		}
		Integer locationId = location == null ? null : location.getLocationId();
		Integer formId = form == null ? null : form.getFormId();
		List<String> whereClauses = new ArrayList<String>();
		whereClauses.add("e.voided = false");
		if (encTypeIds != null)
			whereClauses.add("e.encounter_type in (:encTypeIds)");
		if (locationId != null)
			whereClauses.add("e.location_id = :locationId");
		if (formId != null)
			whereClauses.add("e.form_id = :formId");
		if (fromDate != null)
			whereClauses.add("e.encounter_datetime >= :fromDate");
		if (toDate != null)
			whereClauses.add("e.encounter_datetime <= :toDate");
		List<String> havingClauses = new ArrayList<String>();
		if (minCount != null)
			havingClauses.add("count(*) >= :minCount");
		if (maxCount != null)
			havingClauses.add("count(*) >= :maxCount");
		StringBuilder sb = new StringBuilder();
		sb.append(" select e.patient_id from encounter e ");
		sb.append(" inner join patient p on e.patient_id = p.patient_id and p.voided = false ");
		for (ListIterator<String> i = whereClauses.listIterator(); i.hasNext();) {
			sb.append(i.nextIndex() == 0 ? " where " : " and ");
			sb.append(i.next());
		}
		sb.append(" group by e.patient_id ");
		for (ListIterator<String> i = havingClauses.listIterator(); i.hasNext();) {
			sb.append(i.nextIndex() == 0 ? " having " : " and ");
			sb.append(i.next());
		}
		log.debug("query: " + sb);
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		if (encTypeIds != null)
			query.setParameterList("encTypeIds", encTypeIds);
		if (locationId != null)
			query.setInteger("locationId", locationId);
		if (formId != null)
			query.setInteger("formId", formId);
		if (fromDate != null)
			query.setDate("fromDate", fromDate);
		if (toDate != null)
			query.setDate("toDate", toDate);
		if (minCount != null)
			query.setInteger("minCount", minCount);
		if (maxCount != null)
			query.setInteger("maxCount", maxCount);
		
		return new Cohort(query.list());
	}
	
	/**
	 * within <code>startTime</code> and <code>endTime</code>
	 * 
	 * @param conceptId
	 * @param startTime
	 * @param endTime
	 * @return PatientSet
	 */
	public Cohort getPatientsHavingDateObs(Integer conceptId, Date startTime, Date endTime) {
		StringBuffer sb = new StringBuffer();
		sb.append("select o.person_id from obs o " + "where concept_id = :concept_id ");
		sb.append(" and o.value_datetime between :startValue and :endValue");
		sb.append(" and o.voided = '0'");
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		query.setCacheMode(CacheMode.IGNORE);
		
		query.setInteger("concept_id", conceptId);
		query.setDate("startValue", startTime);
		query.setDate("endValue", endTime);
		
		return new Cohort(query.list());
	}
	
	public Cohort getPatientsHavingNumericObs(Integer conceptId, PatientSetService.TimeModifier timeModifier,
	        PatientSetService.Modifier modifier, Number value, Date fromDate, Date toDate) {
		
		Concept concept = Context.getConceptService().getConcept(conceptId);
		if (!concept.isNumeric()) {
			// throw new IllegalArgumentException(concept + " is not numeric");
		}
		
		StringBuffer sb = new StringBuffer();
		boolean useValue = modifier != null && value != null;
		boolean doSqlAggregation = timeModifier == TimeModifier.MIN || timeModifier == TimeModifier.MAX
		        || timeModifier == TimeModifier.AVG;
		String valueSql = "o.value_numeric";
		boolean doInvert = false;
		
		String dateSql = "";
		if (fromDate != null)
			dateSql += " and o.obs_datetime >= :fromDate ";
		if (toDate != null)
			dateSql += " and o.obs_datetime <= :toDate ";
		
		if (timeModifier == TimeModifier.ANY || timeModifier == TimeModifier.NO) {
			if (timeModifier == TimeModifier.NO)
				doInvert = true;
			sb.append("select o.person_id from obs o " + "where voided = false and concept_id = :concept_id ");
			sb.append(dateSql);
		} else if (timeModifier == TimeModifier.FIRST || timeModifier == TimeModifier.LAST) {
			boolean isFirst = timeModifier == PatientSetService.TimeModifier.FIRST;
			sb.append("select o.person_id " + "from obs o inner join (" + "    select person_id, "
			        + (isFirst ? "min" : "max") + "(obs_datetime) as obs_datetime" + "    from obs"
			        + "    where voided = false and concept_id = :concept_id " + dateSql + "    group by person_id"
			        + ") subq on o.person_id = subq.person_id and o.obs_datetime = subq.obs_datetime "
			        + "where o.voided = false and o.concept_id = :concept_id ");
		} else if (doSqlAggregation) {
			String sqlAggregator = timeModifier.toString();
			valueSql = sqlAggregator + "(o.value_numeric)";
			sb.append("select o.person_id " + "from obs o where o.voided = false and concept_id = :concept_id " + dateSql
			        + "group by o.person_id ");
		} else {
			throw new IllegalArgumentException("TimeModifier '" + timeModifier + "' not recognized");
		}
		
		if (useValue) {
			sb.append(doSqlAggregation ? "having " : " and ");
			sb.append(valueSql + " ");
			sb.append(modifier.getSqlRepresentation() + " :value");
		}
		if (!doSqlAggregation)
			sb.append(" group by o.person_id ");
		
		log.debug("query: " + sb);
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		query.setCacheMode(CacheMode.IGNORE);
		
		query.setInteger("concept_id", conceptId);
		if (useValue) {
			query.setDouble("value", value.doubleValue());
		}
		if (fromDate != null)
			query.setDate("fromDate", fromDate);
		if (toDate != null)
			query.setDate("toDate", fromDate);
		
		Cohort ret;
		if (doInvert) {
			ret = getAllPatients();
			ret.getMemberIds().removeAll(query.list());
		} else {
			ret = new Cohort(query.list());
		}
		
		return ret;
	}
	
	public Cohort getPatientsByCharacteristics(String gender, Date minBirthdate, Date maxBirthdate, Integer minAge,
	        Integer maxAge, Boolean aliveOnly, Boolean deadOnly) throws DAOException {
		return getPatientsByCharacteristics(gender, minBirthdate, maxBirthdate, minAge, maxAge, aliveOnly, deadOnly, null);
	}
	
	public Cohort getPatientsByCharacteristics(String gender, Date minBirthdate, Date maxBirthdate, Integer minAge,
	        Integer maxAge, Boolean aliveOnly, Boolean deadOnly, Date effectiveDate) throws DAOException {
		
		if (effectiveDate == null) {
			effectiveDate = new Date();
		}
		
		StringBuffer queryString = new StringBuffer("select patientId from Patient patient");
		List<String> clauses = new ArrayList<String>();
		
		clauses.add("patient.voided = false");
		
		if (gender != null) {
			gender = gender.toUpperCase();
			clauses.add("patient.gender = :gender");
		}
		if (minBirthdate != null) {
			clauses.add("patient.birthdate >= :minBirthdate");
		}
		if (maxBirthdate != null) {
			clauses.add("patient.birthdate <= :maxBirthdate");
		}
		if (aliveOnly != null && aliveOnly) {
			clauses.add("patient.dead = false");
		}
		if (deadOnly != null && deadOnly) {
			clauses.add("patient.dead = true");
		}
		
		Date maxBirthFromAge = null;
		if (minAge != null) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(effectiveDate);
			cal.add(Calendar.YEAR, -minAge);
			maxBirthFromAge = cal.getTime();
			clauses.add("patient.birthdate <= :maxBirthFromAge");
		}
		Date minBirthFromAge = null;
		if (maxAge != null) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(effectiveDate);
			cal.add(Calendar.YEAR, -(maxAge + 1));
			minBirthFromAge = cal.getTime();
			clauses.add("patient.birthdate > :minBirthFromAge");
		}
		
		clauses.add("(patient.birthdate is null or patient.birthdate <= :effectiveDate)");
		
		boolean first = true;
		for (String clause : clauses) {
			if (first) {
				queryString.append(" where ").append(clause);
				first = false;
			} else {
				queryString.append(" and ").append(clause);
			}
		}
		Query query = sessionFactory.getCurrentSession().createQuery(queryString.toString());
		query.setCacheMode(CacheMode.IGNORE);
		if (gender != null) {
			query.setString("gender", gender);
		}
		if (minBirthdate != null) {
			query.setDate("minBirthdate", minBirthdate);
		}
		if (maxBirthdate != null) {
			query.setDate("maxBirthdate", maxBirthdate);
		}
		if (minAge != null) {
			query.setDate("maxBirthFromAge", maxBirthFromAge);
		}
		if (maxAge != null) {
			query.setDate("minBirthFromAge", minBirthFromAge);
		}
		query.setDate("effectiveDate", effectiveDate);
		
		return new Cohort(query.list());
	}
	
	private static final long MS_PER_YEAR = 365L * 24 * 60 * 60 * 1000L;
	
	@SuppressWarnings("unchecked")
	public Map<Integer, String> getShortPatientDescriptions(Collection<Integer> patientIds) throws DAOException {
		Map<Integer, String> ret = new HashMap<Integer, String>();
		
		Query query = sessionFactory.getCurrentSession().createQuery(
		    "select patient.personId, patient.gender, patient.birthdate from Patient patient where voided = false");
		query.setCacheMode(CacheMode.IGNORE);
		
		List<Object[]> temp = query.list();
		
		long now = System.currentTimeMillis();
		for (Object[] results : temp) {
			if (!patientIds.contains(results[0])) {
				continue;
			}
			StringBuffer sb = new StringBuffer();
			if ("M".equals(results[1])) {
				sb.append("Male");
			} else {
				sb.append("Female");
			}
			Date bd = (Date) results[2];
			if (bd != null) {
				int age = (int) ((now - bd.getTime()) / MS_PER_YEAR);
				sb.append(", ").append(age).append(" years old");
			}
			ret.put((Integer) results[0], sb.toString());
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, Map<String, Object>> getCharacteristics(Cohort patients) throws DAOException {
		Map<Integer, Map<String, Object>> ret = new HashMap<Integer, Map<String, Object>>();
		Collection<Integer> ids = patients.getMemberIds();
		Query query = sessionFactory.getCurrentSession().createQuery(
		    "select patient.personId, patient.gender, patient.birthdate from Patient patient where patient.voided = false");
		query.setCacheMode(CacheMode.IGNORE);
		
		List<Object[]> temp = query.list();
		
		long now = System.currentTimeMillis();
		for (Object[] results : temp) {
			Integer patientId = (Integer) results[0];
			if (!ids.contains(patientId)) {
				continue;
			}
			Map<String, Object> holder = new HashMap<String, Object>();
			holder.put("gender", results[1]);
			Date bd = (Date) results[2];
			if (bd != null) {
				int age = (int) ((now - bd.getTime()) / MS_PER_YEAR);
				holder.put("age_years", age);
				holder.put("birthdate", bd);
			}
			ret.put(patientId, holder);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	/**
	 * fromDate and toDate are both inclusive
	 */
	public Map<Integer, List<Obs>> getObservations(Cohort patients, Concept concept, Date fromDate, Date toDate)
	        throws DAOException {
		Map<Integer, List<Obs>> ret = new HashMap<Integer, List<Obs>>();
		
		/*
		Query query = sessionFactory.getCurrentSession().createQuery("select obs, obs.patientId " +
										  "from Obs obs where obs.conceptId = :conceptId " +
										  " and obs.patientId in :ids " +
										  "order by obs.obsDatetime asc");
		query.setInteger("conceptId", conceptId);
		query.set
		
		List<Object[]> temp = query.list();
		for (Object[] holder : temp) {
			Obs obs = (Obs) holder[0];
			Integer ptId = (Integer) holder[1];
			List<Obs> forPatient = ret.get(ptId);
			if (forPatient == null) {
				forPatient = new ArrayList<Obs>();
				ret.put(ptId, forPatient);
			}
			forPatient.add(obs);
		}
		 */
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Obs.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		criteria.add(Restrictions.eq("concept", concept));
		
		// only add this where clause if patients were passed in
		if (patients != null)
			criteria.add(Restrictions.in("person.personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		criteria.addOrder(org.hibernate.criterion.Order.desc("obsDatetime"));
		log.debug("criteria: " + criteria);
		List<Obs> temp = criteria.list();
		for (Obs obs : temp) {
			Integer ptId = obs.getPersonId();
			List<Obs> forPatient = ret.get(ptId);
			if (forPatient == null) {
				forPatient = new ArrayList<Obs>();
				ret.put(ptId, forPatient);
			}
			forPatient.add(obs);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, List<List<Object>>> getObservationsValues(Cohort patients, Concept c, List<String> attributes,
	        Integer limit, boolean showMostRecentFirst) {
		Map<Integer, List<List<Object>>> ret = new HashMap<Integer, List<List<Object>>>();
		
		List<String> aliases = new Vector<String>();
		Boolean conditional = false;
		
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria("org.openmrs.Obs", "obs");
		criteria.setCacheMode(CacheMode.IGNORE);
		
		List<String> columns = new Vector<String>();
		
		for (String attribute : attributes) {
			List<String> classNames = new Vector<String>();
			if (attribute == null) {
				columns = findObsValueColumnName(c);
				if (columns.size() > 1)
					conditional = true;
				continue;
				//log.debug("c: " + c.getConceptId() + " attribute: " + attribute);
			} else if (attribute.equals("valueDate")) {
				// pass -- same column name
			} else if (attribute.equals("valueTime")) {
				// pass -- same column name
			} else if (attribute.equals("valueDatetime")) {
				// pass -- same column name
			} else if (attribute.equals("obsDatetime")) {
				// pass -- same column name
			} else if (attribute.equals("location")) {
				// pass -- same column name
				classNames.add("obs.location");
				attribute = "location.name";
			} else if (attribute.equals("comment")) {
				// pass -- same column name
			} else if (attribute.equals("encounterType")) {
				classNames.add("obs.encounter");
				classNames.add("encounter.encounterType");
				attribute = "encounterType.name";
			} else if (attribute.equals("provider")) {
				classNames.add("obs.encounter");
				attribute = "encounter.provider";
			} else {
				throw new DAOException("Attribute: " + attribute + " is not recognized. Please add reference in "
				        + this.getClass());
			}
			
			for (String className : classNames) { // if aliasing is necessary
				if (!aliases.contains(className)) { // if we haven't aliased this already
					criteria.createAlias(className, className.split("\\.")[1]);
					aliases.add(className);
				}
			}
			
			columns.add(attribute);
		}
		
		String aliasName = "obs";
		
		// set up the query
		ProjectionList projections = Projections.projectionList();
		projections.add(Projections.property("obs.personId"));
		for (String col : columns) {
			if (col.contains("."))
				projections.add(Projections.property(col));
			else
				projections.add(Projections.property(aliasName + "." + col));
		}
		criteria.setProjection(projections);
		
		// only restrict on patient ids if some were passed in
		if (patients != null)
			criteria.add(Restrictions.in("obs.personId", patients.getMemberIds()));
		
		criteria.add(Expression.eq("obs.concept", c));
		criteria.add(Expression.eq("obs.voided", false));
		
		if (showMostRecentFirst)
			criteria.addOrder(org.hibernate.criterion.Order.desc("obs.obsDatetime"));
		else
			criteria.addOrder(org.hibernate.criterion.Order.asc("obs.obsDatetime"));
		
		long start = System.currentTimeMillis();
		List<Object[]> rows = criteria.list();
		log.debug("Took: " + (System.currentTimeMillis() - start) + " ms to run the patient/obs query");
		
		// set up the return map
		for (Object[] rowArray : rows) {
			//log.debug("row[0]: " + row[0] + " row[1]: " + row[1] + (row.length > 2 ? " row[2]: " + row[2] : ""));
			Integer ptId = (Integer) rowArray[0];
			
			List<List<Object>> oldArr = ret.get(ptId);
			
			// if we have already fetched all of the results the user wants 
			if (limit != null && limit > 0 && oldArr != null && oldArr.size() >= limit) {
				// the user provided a limit value and this patient already has more than
				// that number of values.
				// do nothing with this row
			} else {
				Boolean tmpConditional = conditional.booleanValue();
				
				// get all columns
				int index = 1;
				List<Object> row = new Vector<Object>();
				while (index < rowArray.length) {
					Object value = rowArray[index++];
					if (tmpConditional) {
						if (index == 2 && value != null) // skip null first value if we must
							row.add(value);
						else
							row.add(rowArray[index]);
						tmpConditional = false;
						index++; // increment counter for next column.  (Skips over value_concept)
					} else
						row.add(value == null ? "" : value);
				}
				
				// if we haven't seen a different row for this patient already:
				if (oldArr == null) {
					List<List<Object>> arr = new Vector<List<Object>>();
					arr.add(row);
					ret.put(ptId, arr);
				}
				// if we have seen a row for this patient already
				else {
					oldArr.add(row);
					ret.put(ptId, oldArr);
				}
			}
		}
		
		return ret;
		
	}
	
	public static List<String> findObsValueColumnName(Concept c) {
		String abbrev = c.getDatatype().getHl7Abbreviation();
		List<String> columns = new Vector<String>();
		
		if (abbrev.equals("BIT"))
			columns.add("valueCoded");
		else if (abbrev.equals("CWE")) {
			columns.add("valueDrug");
			columns.add("valueCoded");
		} else if (abbrev.equals("NM") || abbrev.equals("SN"))
			columns.add("valueNumeric");
		else if (abbrev.equals("DT") || abbrev.equals("TM") || abbrev.equals("TS"))
			columns.add("valueDatetime");
		else if (abbrev.equals("ST"))
			columns.add("valueText");
		
		return columns;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, Encounter> getEncountersByType(Cohort patients, List<EncounterType> encTypes) {
		Map<Integer, Encounter> ret = new HashMap<Integer, Encounter>();
		
		// default query
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Encounter.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// this "where clause" is only necessary if patients were passed in
		if (patients != null && patients.size() > 0)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		
		if (encTypes != null && encTypes.size() > 0)
			criteria.add(Restrictions.in("encounterType", encTypes));
		
		criteria.addOrder(org.hibernate.criterion.Order.desc("patient.personId"));
		criteria.addOrder(org.hibernate.criterion.Order.desc("encounterDatetime"));
		
		List<Encounter> encounters = criteria.list();
		
		// set up the return map
		for (Encounter enc : encounters) {
			Integer ptId = enc.getPatientId();
			if (!ret.containsKey(ptId))
				ret.put(ptId, enc);
		}
		
		return ret;
	}
	
	/**
	 * Gets a list of encounters associated with the given form, filtered by the given patient set.
	 * 
	 * @param patients the patients to filter by (null will return all encounters for all patients)
	 * @param forms the forms to filter by
	 */
	@SuppressWarnings("unchecked")
	public List<Encounter> getEncountersByForm(Cohort patients, List<Form> forms) {
		
		// default query
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Encounter.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// this "where clause" is only necessary if patients were passed in
		if (patients != null && patients.size() > 0)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		
		if (forms != null && forms.size() > 0)
			criteria.add(Restrictions.in("form", forms));
		
		criteria.addOrder(org.hibernate.criterion.Order.desc("patient.personId"));
		criteria.addOrder(org.hibernate.criterion.Order.desc("encounterDatetime"));
		
		return criteria.list();
		
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, Object> getEncounterAttrsByType(Cohort patients, List<EncounterType> encTypes, String attr,
	        Boolean earliestFirst) {
		Map<Integer, Object> ret = new HashMap<Integer, Object>();
		
		// default query
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Encounter.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// this "where clause" is only necessary if patients were specified
		if (patients != null)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		
		if (encTypes != null && encTypes.size() > 0)
			criteria.add(Restrictions.in("encounterType", encTypes));
		
		criteria.setProjection(Projections.projectionList().add(Projections.property("patient.personId")).add(
		    Projections.property(attr)));
		
		criteria.addOrder(org.hibernate.criterion.Order.desc("patient.personId"));
		
		if (earliestFirst)
			criteria.addOrder(org.hibernate.criterion.Order.asc("encounterDatetime"));
		else
			criteria.addOrder(org.hibernate.criterion.Order.desc("encounterDatetime"));
		
		List<Object[]> attrs = criteria.list();
		
		// set up the return map
		for (Object[] row : attrs) {
			Integer ptId = (Integer) row[0];
			if (!ret.containsKey(ptId))
				ret.put(ptId, row[1]);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, Encounter> getEncounters(Cohort patients) {
		Map<Integer, Encounter> ret = new HashMap<Integer, Encounter>();
		
		// default query
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Encounter.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// only include this where clause if patients were passed in
		if (patients != null)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		
		criteria.addOrder(org.hibernate.criterion.Order.desc("patient.personId"));
		criteria.addOrder(org.hibernate.criterion.Order.desc("encounterDatetime"));
		
		List<Encounter> encounters = criteria.list();
		
		// set up the return map
		for (Encounter enc : encounters) {
			Integer ptId = enc.getPatientId();
			if (!ret.containsKey(ptId))
				ret.put(ptId, enc);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, Encounter> getFirstEncountersByType(Cohort patients, List<EncounterType> types) {
		Map<Integer, Encounter> ret = new HashMap<Integer, Encounter>();
		
		// default query
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Encounter.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// this "where clause" is only needed if patients were specified
		if (patients != null)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		
		if (types != null && types.size() > 0)
			criteria.add(Restrictions.in("encounterType", types));
		
		criteria.addOrder(org.hibernate.criterion.Order.desc("patient.personId"));
		criteria.addOrder(org.hibernate.criterion.Order.asc("encounterDatetime"));
		
		List<Encounter> encounters = criteria.list();
		
		// set up the return map
		for (Encounter enc : encounters) {
			Integer ptId = enc.getPatientId();
			if (!ret.containsKey(ptId))
				ret.put(ptId, enc);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, Object> getPatientAttributes(Cohort patients, String className, String property, boolean returnAll)
	        throws DAOException {
		Map<Integer, Object> ret = new HashMap<Integer, Object>();
		
		className = "org.openmrs." + className;
		
		// default query
		Criteria criteria = null;
		
		// make 'patient.**' reference 'patient' like alias instead of object
		if (className.equals("org.openmrs.Patient"))
			criteria = sessionFactory.getCurrentSession().createCriteria("org.openmrs.Patient", "patient");
		else if (className.equals("org.openmrs.Person"))
			criteria = sessionFactory.getCurrentSession().createCriteria("org.openmrs.Person", "person");
		else
			criteria = sessionFactory.getCurrentSession().createCriteria(className);
		
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// set up the query
		ProjectionList projectionList = Projections.projectionList();
		
		// if Person, PersonName, or PersonAddress
		if (className.contains("Person")) {
			projectionList.add(Projections.property("person.personId"));
			projectionList.add(Projections.property(property));
			
			if (patients != null)
				criteria.add(Restrictions.in("person.personId", patients.getMemberIds()));
			
			// do not include voided person rows
			if (className.equals("org.openmrs.Person"))
				// the voided column on the person table is mapped to the person object 
				// through the getPersonVoided() to distinguish it from patient/user.voided 
				criteria.add(Expression.eq("personVoided", false));
			else
				// this is here to support PersonName and PersonAddress
				criteria.add(Expression.eq("voided", false));
		}
		// if one of the Patient tables
		else {
			projectionList.add(Projections.property("patient.personId"));
			projectionList.add(Projections.property(property));
			
			if (patients != null)
				criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
			
			// do not include voided patients
			criteria.add(Expression.eq("voided", false));
		}
		criteria.setProjection(projectionList);
		
		// add 'preferred' sort order if necessary
		try {
			boolean hasPreferred = false;
			for (Field f : Class.forName(className).getDeclaredFields()) {
				if (f.getName().equals("preferred"))
					hasPreferred = true;
			}
			
			if (hasPreferred)
				criteria.addOrder(org.hibernate.criterion.Order.desc("preferred"));
		}
		catch (ClassNotFoundException e) {
			log.warn("Class not found: " + className);
		}
		
		criteria.addOrder(org.hibernate.criterion.Order.desc("dateCreated"));
		List<Object[]> rows = criteria.list();
		
		// set up the return map
		if (returnAll) {
			for (Object[] row : rows) {
				Integer ptId = (Integer) row[0];
				Object columnValue = row[1];
				if (!ret.containsKey(ptId)) {
					Object[] arr = { columnValue };
					ret.put(ptId, arr);
				} else {
					Object[] oldArr = (Object[]) ret.get(ptId);
					Object[] newArr = new Object[oldArr.length + 1];
					System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
					newArr[oldArr.length] = columnValue;
					ret.put(ptId, newArr);
				}
			}
		} else {
			for (Object[] row : rows) {
				Integer ptId = (Integer) row[0];
				Object columnValue = row[1];
				if (!ret.containsKey(ptId))
					ret.put(ptId, columnValue);
			}
		}
		
		return ret;
	}
	
	/**
	 * @see org.openmrs.api.db.PatientSetDAO#getPersonAttributes(org.openmrs.Cohort,
	 *      java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
	 */
	@SuppressWarnings("unchecked")
	public Map<Integer, Object> getPersonAttributes(Cohort patients, String attributeTypeName, String joinClass,
	        String joinProperty, String outputColumn, boolean returnAll) {
		Map<Integer, Object> ret = new HashMap<Integer, Object>();
		
		StringBuilder queryString = new StringBuilder();
		
		// set up the query
		queryString.append("select attr.person.personId, ");
		
		if (joinClass != null && joinProperty != null && outputColumn != null) {
			queryString.append("joinedClass.");
			queryString.append(outputColumn);
			queryString.append(" from PersonAttribute attr, PersonAttributeType t, ");
			queryString.append(joinClass);
			queryString.append(" joinedClass where t = attr.attributeType ");
			queryString.append("and attr.value = joinedClass.");
			queryString.append(joinProperty + " ");
		} else
			queryString.append("attr.value from PersonAttribute attr, PersonAttributeType t where t = attr.attributeType ");
		
		queryString.append("and t.name = :typeName ");
		queryString.append("order by attr.voided asc, attr.dateCreated desc");
		
		Query query = sessionFactory.getCurrentSession().createQuery(queryString.toString());
		query.setString("typeName", attributeTypeName);
		
		log.debug("query: " + queryString);
		
		List<Object[]> rows = query.list();
		
		// set up the return map
		for (Object[] row : rows) {
			Integer ptId = (Integer) row[0];
			if (patients == null || patients.contains(ptId)) {
				if (returnAll) {
					Object columnValue = row[1];
					if (!ret.containsKey(ptId)) {
						Object[] arr = { columnValue };
						ret.put(ptId, arr);
					} else {
						Object[] oldArr = (Object[]) ret.get(ptId);
						Object[] newArr = new Object[oldArr.length + 1];
						System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
						newArr[oldArr.length] = columnValue;
						ret.put(ptId, newArr);
					}
				} else {
					Object columnValue = row[1];
					if (!ret.containsKey(ptId))
						ret.put(ptId, columnValue);
				}
			}
		}
		return ret;
	}
	
	public Cohort getPatientsHavingTextObs(Integer conceptId, String value, TimeModifier timeModifier) throws DAOException {
		Query query;
		StringBuffer sb = new StringBuffer();
		sb.append("select o.person_id from obs o ");
		
		if (timeModifier != null) {
			if (timeModifier.equals(TimeModifier.LAST)) {
				log.debug("timeModifier is NOT NULL, and appears to be LAST, so we'll try to add a subquery");
				sb.append("inner join (select person_id, max(obs_datetime) as obs_datetime from obs where ");
				sb
				        .append("concept_id = :concept_id group by person_id) sub on o.person_id = sub.person_id and o.obs_datetime = sub.obs_datetime ");
			} else {
				log.debug("timeModifier is NOT NULL, and appears to not be LAST, so we won't do anything");
			}
		} else {
			log.debug("timeModifier is NULL, skipping to full query");
		}
		
		sb.append("where o.concept_id = :concept_id ");
		boolean useVal = false;
		if (value != null) {
			sb.append("and o.value_text = :value ");
			useVal = true;
		} else {
			sb.append("and o.value_text is not null ");
		}
		sb.append("group by o.person_id ");
		
		query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		query.setCacheMode(CacheMode.IGNORE);
		query.setInteger("concept_id", conceptId);
		if (useVal) {
			query.setString("value", value);
		}
		
		return new Cohort(query.list());
	}
	
	public Cohort getPatientsHavingLocation(Integer locationId, PatientSetService.PatientLocationMethod method) {
		StringBuffer sb = new StringBuffer();
		boolean argumentAsString = false;
		if (method == PatientLocationMethod.ANY_ENCOUNTER) {
			sb.append(" select e.patient_id from ");
			sb.append(" encounter e ");
			sb.append(" inner join patient p on e.patient_id = p.patient_id and p.voided = false ");
			sb.append(" where e.location_id = :location_id ");
			sb.append(" group by e.patient_id ");
		} else if (method == PatientLocationMethod.EARLIEST_ENCOUNTER) {
			sb.append(" select e.patient_id ");
			sb.append(" from encounter e ");
			sb.append("   inner join patient p on e.patient_id = p.patient_id and p.voided = false ");
			sb.append("   inner join (");
			sb.append("       select patient_id, min(encounter_datetime) as earliest ");
			sb.append("       from encounter ");
			sb.append("       group by patient_id) subq ");
			sb.append("     on e.patient_id = subq.patient_id and e.encounter_datetime = subq.earliest ");
			sb.append(" where e.location_id = :location_id ");
			sb.append(" group by e.patient_id ");
		} else if (method == PatientLocationMethod.LATEST_ENCOUNTER) {
			sb.append(" select e.patient_id ");
			sb.append(" from encounter e ");
			sb.append("   inner join patient p on e.patient_id = p.patient_id and p.voided = false ");
			sb.append("   inner join (");
			sb.append("       select patient_id, max(encounter_datetime) as earliest ");
			sb.append("       from encounter ");
			sb.append("       group by patient_id) subq ");
			sb.append("     on e.patient_id = subq.patient_id and e.encounter_datetime = subq.earliest ");
			sb.append(" where e.location_id = :location_id ");
			sb.append(" group by e.patient_id ");
		} else {
			sb.append(" select patient_id from patient p, person_attribute attr, person_attribute_type type ");
			sb.append(" where type.name = 'Health Center' ");
			sb.append(" and type.person_attribute_type_id = attr.person_attribute_type_id ");
			sb.append(" and attr.value = :location_id ");
			sb.append(" and attr.person_id = p.patient_id ");
			sb.append(" and attr.voided = false ");
			sb.append(" and p.voided = false ");
			argumentAsString = true;
		}
		log.debug("query: " + sb);
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		
		if (argumentAsString) {
			query.setString("location_id", locationId.toString());
		} else {
			query.setInteger("location_id", locationId);
		}
		
		return new Cohort(query.list());
	}
	
	public Cohort convertPatientIdentifier(List<String> identifiers) throws DAOException {
		
		StringBuffer sb = new StringBuffer();
		sb.append("select distinct(patient_id) from patient_identifier p ");
		sb.append("where identifier in (:identifiers)");
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		query.setCacheMode(CacheMode.IGNORE);
		query.setParameterList("identifiers", identifiers, new StringType());
		return new Cohort(query.list());
	}
	
	@SuppressWarnings("unchecked")
	public List<Patient> getPatients(Collection<Integer> patientIds) throws DAOException {
		List<Patient> ret = new ArrayList<Patient>();
		
		if (!patientIds.isEmpty()) {
			Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Patient.class);
			criteria.setCacheMode(CacheMode.IGNORE);
			criteria.add(Restrictions.in("patientId", patientIds));
			criteria.add(Restrictions.eq("voided", false));
			log.debug("criteria: " + criteria);
			List<Patient> temp = criteria.list();
			for (Patient p : temp) {
				ret.add(p);
			}
		}
		
		return ret;
	}
	
	/**
	 * Returns a Map from patientId to a Collection of drugIds for drugs active for the patients on
	 * that date If patientIds is null then do this for all patients Does not return anything for
	 * voided patients
	 * 
	 * @throws DAOException
	 */
	@SuppressWarnings("unchecked")
	public Map<Integer, Collection<Integer>> getActiveDrugIds(Collection<Integer> patientIds, Date fromDate, Date toDate)
	        throws DAOException {
		HashSet<Integer> idsLookup = patientIds == null ? null
		        : (patientIds instanceof HashSet ? (HashSet<Integer>) patientIds : new HashSet<Integer>(patientIds));
		
		Map<Integer, Collection<Integer>> ret = new HashMap<Integer, Collection<Integer>>();
		
		List<String> whereClauses = new ArrayList<String>();
		whereClauses.add("o.voided = false");
		if (toDate != null)
			whereClauses.add("o.start_date <= :toDate");
		if (fromDate != null) {
			whereClauses.add("(o.auto_expire_date is null or o.auto_expire_date > :fromDate)");
			whereClauses.add("(o.discontinued_date is null or o.discontinued_date > :fromDate)");
		}
		
		String sql = "select o.patient_id, d.drug_inventory_id " + "from orders o "
		        + "    inner join patient p on o.patient_id = p.patient_id and p.voided = false "
		        + "    inner join drug_order d on o.order_id = d.order_id ";
		for (ListIterator<String> i = whereClauses.listIterator(); i.hasNext();) {
			sql += (i.nextIndex() == 0 ? " where " : " and ");
			sql += i.next();
		}
		
		log.debug("sql= " + sql);
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sql);
		query.setCacheMode(CacheMode.IGNORE);
		
		if (toDate != null)
			query.setDate("toDate", toDate);
		if (fromDate != null)
			query.setDate("fromDate", fromDate);
		
		List<Object[]> results = (List<Object[]>) query.list();
		for (Object[] row : results) {
			Integer patientId = (Integer) row[0];
			if (idsLookup == null || idsLookup.contains(patientId)) {
				Integer drugId = (Integer) row[1];
				Collection<Integer> drugIds = ret.get(patientId);
				if (drugIds == null) {
					drugIds = new HashSet<Integer>();
					ret.put(patientId, drugIds);
				}
				drugIds.add(drugId);
			}
		}
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, PatientState> getCurrentStates(Cohort ps, ProgramWorkflow wf) throws DAOException {
		Map<Integer, PatientState> ret = new HashMap<Integer, PatientState>();
		
		if (ps == null || ps.getMemberIds().size() > 0) {
			Date now = new Date();
			
			Criteria criteria = sessionFactory.getCurrentSession().createCriteria(PatientState.class);
			criteria.setFetchMode("patient", FetchMode.JOIN);
			criteria.setCacheMode(CacheMode.IGNORE);
			//criteria.add(Restrictions.in("patientProgram.patient.personId", ids));
			
			// only include this where clause if patients were passed in
			if (ps != null)
				criteria.createCriteria("patientProgram").add(Restrictions.in("patient.personId", ps.getMemberIds()));
			
			//criteria.add(Restrictions.eq("state.programWorkflow", wf));
			criteria.createCriteria("state").add(Restrictions.eq("programWorkflow", wf));
			criteria.add(Restrictions.eq("voided", false));
			criteria.add(Restrictions.or(Restrictions.isNull("startDate"), Restrictions.le("startDate", now)));
			criteria.add(Restrictions.or(Restrictions.isNull("endDate"), Restrictions.ge("endDate", now)));
			log.debug("criteria: " + criteria);
			List<PatientState> temp = criteria.list();
			for (PatientState state : temp) {
				Integer ptId = state.getPatientProgram().getPatient().getPatientId();
				ret.put(ptId, state);
			}
		}
		return ret;
	}
	
	/**
	 * This method assumes the patient is not simultaneously enrolled in the program more than once.
	 * if (includeVoided == true) then include voided programs if (includePast == true) then include
	 * program which are already complete In all cases this only returns the latest program
	 * enrollment for each patient.
	 */
	@SuppressWarnings("unchecked")
	public Map<Integer, PatientProgram> getPatientPrograms(Cohort ps, Program program, boolean includeVoided,
	        boolean includePast) throws DAOException {
		Map<Integer, PatientProgram> ret = new HashMap<Integer, PatientProgram>();
		
		Date now = new Date();
		
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(PatientProgram.class);
		criteria.setFetchMode("patient", FetchMode.JOIN);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// this "where clause" is only necessary if patients were passed in
		if (ps != null)
			criteria.add(Restrictions.in("patient.personId", ps.getMemberIds()));
		
		criteria.add(Restrictions.eq("program", program));
		if (!includeVoided)
			criteria.add(Restrictions.eq("voided", false));
		criteria.add(Restrictions.or(Restrictions.isNull("dateEnrolled"), Restrictions.le("dateEnrolled", now)));
		if (!includePast)
			criteria.add(Restrictions.or(Restrictions.isNull("dateCompleted"), Restrictions.ge("dateCompleted", now)));
		log.debug("criteria: " + criteria);
		List<PatientProgram> temp = criteria.list();
		for (PatientProgram prog : temp) {
			Integer ptId = prog.getPatient().getPatientId();
			ret.put(ptId, prog);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, List<DrugOrder>> getCurrentDrugOrders(Cohort patients, List<Concept> drugConcepts)
	        throws DAOException {
		Map<Integer, List<DrugOrder>> ret = new HashMap<Integer, List<DrugOrder>>();
		
		Date now = new Date();
		
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(DrugOrder.class);
		criteria.setFetchMode("patient", FetchMode.JOIN);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// this "where clause" is only necessary if patients were passed in
		if (patients != null)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		//criteria.add(Restrictions.in("encounter.patient.personId", ids));
		//criteria.createCriteria("encounter").add(Restrictions.in("patient.personId", ids));
		if (drugConcepts != null)
			criteria.add(Restrictions.in("concept", drugConcepts));
		criteria.add(Restrictions.eq("voided", false));
		criteria.add(Restrictions.le("startDate", now));
		criteria.add(Restrictions.or(Restrictions.and(Restrictions.eq("discontinued", false), Restrictions.or(Restrictions
		        .isNull("autoExpireDate"), Restrictions.gt("autoExpireDate", now))), Restrictions.and(Restrictions.eq(
		    "discontinued", true), Restrictions.gt("discontinuedDate", now))));
		criteria.addOrder(org.hibernate.criterion.Order.asc("startDate"));
		log.debug("criteria: " + criteria);
		List<DrugOrder> temp = criteria.list();
		for (DrugOrder regimen : temp) {
			Integer ptId = regimen.getPatient().getPatientId();
			List<DrugOrder> list = ret.get(ptId);
			if (list == null) {
				list = new ArrayList<DrugOrder>();
				ret.put(ptId, list);
			}
			list.add(regimen);
		}
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, List<DrugOrder>> getDrugOrders(Cohort patients, List<Concept> drugConcepts) throws DAOException {
		Map<Integer, List<DrugOrder>> ret = new HashMap<Integer, List<DrugOrder>>();
		if (patients != null && patients.size() == 0)
			return ret;
		
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(DrugOrder.class);
		criteria.setFetchMode("patient", FetchMode.JOIN);
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// only include this where clause if patients were passed in
		if (patients != null)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		if (drugConcepts != null)
			criteria.add(Restrictions.in("concept", drugConcepts));
		criteria.add(Restrictions.eq("voided", false));
		criteria.addOrder(org.hibernate.criterion.Order.asc("startDate"));
		log.debug("criteria: " + criteria);
		List<DrugOrder> temp = criteria.list();
		for (DrugOrder regimen : temp) {
			Integer ptId = regimen.getPatient().getPatientId();
			List<DrugOrder> list = ret.get(ptId);
			if (list == null) {
				list = new ArrayList<DrugOrder>();
				ret.put(ptId, list);
			}
			list.add(regimen);
		}
		return ret;
	}
	
	/* 
	 * This is a small hack to make the relationships work right in Neal's report code. It will be refactored
	 * when I implement a relationship type filter for the cohort builder. -DJ
	 */
	@SuppressWarnings("unchecked")
	public Map<Integer, List<Person>> getRelatives(Cohort patients, RelationshipType relType, boolean forwards) {
		if (relType == null)
			throw new IllegalArgumentException("Must give a relationship type");
		Map<Integer, List<Person>> ret = new HashMap<Integer, List<Person>>();
		if (patients != null)
			if (patients.size() == 0)
				return ret;
		
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Relationship.class);
		criteria.add(Restrictions.eq("voided", false));
		if (patients != null) {
			if (forwards) {
				criteria.add(Restrictions.in("personA.personId", patients.getMemberIds()));
			} else {
				criteria.add(Restrictions.in("personB.personId", patients.getMemberIds()));
			}
		}
		log.debug("criteria: " + criteria);
		List<Relationship> rels = (List<Relationship>) criteria.list();
		for (Relationship rel : rels) {
			Person fromPerson = forwards ? rel.getPersonA() : rel.getPersonB();
			Person toPerson = forwards ? rel.getPersonB() : rel.getPersonA();
			List<Person> holder = (List<Person>) ret.get(fromPerson.getPersonId());
			if (holder == null) {
				holder = new ArrayList<Person>();
				ret.put(fromPerson.getPersonId(), holder);
			}
			holder.add(toPerson);
		}
		
		return ret;
	}
	
	@SuppressWarnings("unchecked")
	public Map<Integer, List<Relationship>> getRelationships(Cohort patients, RelationshipType relType) {
		Map<Integer, List<Relationship>> ret = new HashMap<Integer, List<Relationship>>();
		
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Relationship.class);
		criteria.setCacheMode(CacheMode.IGNORE);
		if (relType != null)
			criteria.add(Restrictions.eq("relationshipType", relType));
		
		// this "where clause" is only useful if patients were passed in
		if (patients != null)
			criteria.createCriteria("personB").add(Restrictions.in("personId", patients.getMemberIds()));
		
		criteria.add(Restrictions.eq("voided", false));
		log.debug("criteria: " + criteria);
		List<Relationship> temp = criteria.list();
		for (Relationship rel : temp) {
			Integer ptId = rel.getPersonB().getPersonId();
			List<Relationship> rels = ret.get(ptId);
			if (rels == null) {
				rels = new ArrayList<Relationship>();
				ret.put(ptId, rels);
			}
			rels.add(rel);
		}
		return ret;
	}
	
	public Cohort getPatientsHavingPersonAttribute(PersonAttributeType attribute, String value) {
		StringBuilder sb = new StringBuilder();
		sb.append(" select pat.patient_id ");
		sb.append(" from person p ");
		sb.append(" inner join patient pat on pat.patient_id = p.person_id and pat.voided = false ");
		sb.append(" inner join person_attribute a on p.person_id = a.person_id and a.voided = false ");
		sb.append(" where p.voided = false ");
		if (attribute != null)
			sb.append(" and a.person_attribute_type_id = :typeId ");
		if (value != null)
			sb.append(" and a.value = :value ");
		sb.append(" group by pat.patient_id ");
		log.debug("query: " + sb);
		
		Query query = sessionFactory.getCurrentSession().createSQLQuery(sb.toString());
		if (attribute != null)
			query.setInteger("typeId", attribute.getPersonAttributeTypeId());
		if (value != null)
			query.setString("value", value);
		
		return new Cohort(query.list());
	}
	
	public Cohort getPatientsHavingDrugOrder(List<Drug> drugList, List<Concept> drugConceptList, Date startDateFrom,
	        Date startDateTo, Date stopDateFrom, Date stopDateTo, Boolean discontinued, List<Concept> discontinuedReason) {
		if (drugList != null && drugList.size() == 0)
			drugList = null;
		if (drugConceptList != null && drugConceptList.size() == 0)
			drugConceptList = null;
		StringBuilder sb = new StringBuilder();
		sb.append(" select distinct patient.id from DrugOrder where voided = false and patient.voided = false ");
		if (drugList != null)
			sb.append(" and drug.id in (:drugIdList) ");
		if (drugConceptList != null)
			sb.append(" and concept.id in (:drugConceptIdList) ");
		if (startDateFrom != null && startDateTo != null) {
			sb.append(" and startDate between :startDateFrom and :startDateTo ");
		} else {
			if (startDateFrom != null)
				sb.append(" and startDate >= :startDateFrom ");
			if (startDateTo != null)
				sb.append(" and startDate <= :startDateTo ");
		}
		if (discontinuedReason != null && discontinuedReason.size() > 0)
			sb.append(" and discontinuedReason.id in (:discontinuedReasonIdList) ");
		if (discontinued != null) {
			sb.append(" and discontinued = :discontinued ");
			if (discontinued == true) {
				if (stopDateFrom != null && stopDateTo != null) {
					sb.append(" and discontinuedDate between :stopDateFrom and :stopDateTo ");
				} else {
					if (stopDateFrom != null)
						sb.append(" and discontinuedDate >= :stopDateFrom ");
					if (stopDateTo != null)
						sb.append(" and discontinuedDate <= :stopDateTo ");
				}
			} else { // discontinued == false
				if (stopDateFrom != null && stopDateTo != null) {
					sb.append(" and autoExpireDate between :stopDateFrom and :stopDateTo ");
				} else {
					if (stopDateFrom != null)
						sb.append(" and autoExpireDate >= :stopDateFrom ");
					if (stopDateTo != null)
						sb.append(" and autoExpireDate <= :stopDateTo ");
				}
			}
		} else { // discontinued == null, so we need either
			if (stopDateFrom != null && stopDateTo != null) {
				sb.append(" and coalesce(discontinuedDate, autoExpireDate) between :stopDateFrom and :stopDateTo ");
			} else {
				if (stopDateFrom != null)
					sb.append(" and coalesce(discontinuedDate, autoExpireDate) >= :stopDateFrom ");
				if (stopDateTo != null)
					sb.append(" and coalesce(discontinuedDate, autoExpireDate) <= :stopDateTo ");
			}
		}
		log.debug("sql = " + sb);
		Query query = sessionFactory.getCurrentSession().createQuery(sb.toString());
		
		if (drugList != null) {
			List<Integer> ids = new ArrayList<Integer>();
			for (Drug d : drugList)
				ids.add(d.getDrugId());
			query.setParameterList("drugIdList", ids);
		}
		if (drugConceptList != null) {
			List<Integer> ids = new ArrayList<Integer>();
			for (Concept c : drugConceptList)
				ids.add(c.getConceptId());
			query.setParameterList("drugConceptIdList", ids);
		}
		if (startDateFrom != null)
			query.setDate("startDateFrom", startDateFrom);
		if (startDateTo != null)
			query.setDate("startDateTo", startDateTo);
		if (stopDateFrom != null)
			query.setDate("stopDateFrom", stopDateFrom);
		if (stopDateTo != null)
			query.setDate("stopDateTo", stopDateTo);
		if (discontinued != null)
			query.setBoolean("discontinued", discontinued);
		if (discontinuedReason != null && discontinuedReason.size() > 0) {
			List<Integer> ids = new ArrayList<Integer>();
			for (Concept c : discontinuedReason)
				ids.add(c.getConceptId());
			query.setParameterList("discontinuedReasonIdList", ids);
		}
		
		return new Cohort(query.list());
	}
	
	/**
	 * @param patients
	 * @param types List<PatientIdentifierTypes> of types to get
	 * @return Map of {@link PatientIdentifier}s
	 */
	@SuppressWarnings("unchecked")
	public Map<Integer, String> getPatientIdentifierByType(Cohort patients, List<PatientIdentifierType> types) {
		Map<Integer, String> patientIdentifiers = new HashMap<Integer, String>();
		
		// default query
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(PatientIdentifier.class);
		
		// only get the "identifier" and "patientId" columns
		ProjectionList projections = Projections.projectionList();
		projections.add(Projections.property("identifier"));
		projections.add(Projections.property("patient.personId"));
		criteria.setProjection(projections);
		
		criteria.setCacheMode(CacheMode.IGNORE);
		
		// Add patient restriction if necessary
		if (patients != null)
			criteria.add(Restrictions.in("patient.personId", patients.getMemberIds()));
		
		// all identifiers must be non-voided
		criteria.add(Restrictions.eq("voided", false));
		
		// Add identifier type filter
		if (types != null && types.size() > 0)
			criteria.add(Restrictions.in("identifierType", types));
		
		// Order by ID
		criteria.addOrder(org.hibernate.criterion.Order.desc("patient.personId"));
		
		List<Object[]> rows = criteria.list();
		
		// set up the return map
		for (Object[] row : rows) {
			String identifier = (String) row[0];
			Integer patientId = (Integer) row[1];
			if (!patientIdentifiers.containsKey(patientId))
				patientIdentifiers.put(patientId, identifier);
		}
		
		return patientIdentifiers;
	}
	
	/**
	 * 
	 * @see org.openmrs.api.db.PatientSetDAO#getPatientsByRelationship(org.openmrs.RelationshipType,
	 *      boolean, boolean, org.openmrs.Person)
	 */
	public Cohort getPatientsByRelationship(RelationshipType relType, boolean includeAtoB, boolean includeBtoA, Person target) {
		
		// since members of a relationship aren't necessarily Patients, but we're supposed to be returning
		// just patients, get all patients first to compare against:
		Cohort allPatients = getAllPatients();
		
		if (relType != null) {
			if (includeAtoB && includeBtoA) {
				String hql = "select personA.id, personB.id from Relationship where relationshipType = :relType";
				if (target != null)
					hql += " and (personA.id = :targetId or personB.id = :targetId)";
				Query q = sessionFactory.getCurrentSession().createQuery(hql);
				q.setParameter("relType", relType);
				if (target != null)
					q.setInteger("targetId", target.getPersonId());
				Cohort ret = new Cohort();
				for (Object[] o : (List<Object[]>) q.list()) {
					ret.addMember((Integer) o[0]);
					ret.addMember((Integer) o[1]);
				}
				ret.removeMember(target.getPersonId());
				return Cohort.intersect(allPatients, ret);
			} else if (includeAtoB) {
				String hql = "select personA.id from Relationship where relationshipType = :relType";
				if (target != null)
					hql += " and personB.id = :targetId";
				Query q = sessionFactory.getCurrentSession().createQuery(hql);
				q.setParameter("relType", relType);
				if (target != null)
					q.setInteger("targetId", target.getPersonId());
				Cohort ret = new Cohort();
				for (Integer id : (List<Integer>) q.list())
					ret.addMember(id);
				return Cohort.intersect(allPatients, ret);
			} else if (includeBtoA) {
				String hql = "select personB.id from Relationship where relationshipType = :relType";
				if (target != null)
					hql += " and personA.id = :targetId";
				Query q = sessionFactory.getCurrentSession().createQuery(hql);
				q.setParameter("relType", relType);
				if (target != null)
					q.setInteger("targetId", target.getPersonId());
				Cohort ret = new Cohort();
				for (Integer id : (List<Integer>) q.list())
					ret.addMember(id);
				return Cohort.intersect(allPatients, ret);
			} else {
				return new Cohort();
			}
		} else {
			if (target != null) {
				Cohort ret = new Cohort();
				Integer ptId = target.getPersonId();
				Query query = sessionFactory.getCurrentSession().createSQLQuery(
				    "select person_a, person_b from relationship where person_a = :ptId or person_b = :ptId");
				query.setInteger("ptId", ptId);
				for (Object[] o : (List<Object[]>) query.list()) {
					ret.addMember((Integer) o[0]);
					ret.addMember((Integer) o[1]);
				}
				// don't include the target patient
				ret.removeMember(ptId);
				return Cohort.intersect(allPatients, ret);
			} else {
				// get everyone at either end of any relationship, and determine if they're a patient
				Cohort ret = new Cohort();
				Query query = sessionFactory.getCurrentSession().createSQLQuery(
				    "select person_a, person_b from relationship");
				for (Object[] o : (List<Object[]>) query.list()) {
					ret.addMember((Integer) o[0]);
					ret.addMember((Integer) o[1]);
				}
				return Cohort.intersect(allPatients, ret);
			}
		}
	}
	
	@Override
	public Integer getCountOfPatients() {
		Query query = sessionFactory.getCurrentSession().createQuery("select count(*) from Patient where voided = '0'");
		return new Integer(query.uniqueResult().toString());
	}
	
	@Override
	public Cohort getPatients(Integer start, Integer size) {
		Query query = sessionFactory.getCurrentSession().createQuery(
		    "select distinct patientId from Patient p where p.voided = '0'");
		
		if (start != null)
			query.setFirstResult(start);
		
		if (size != null)
			query.setMaxResults(size);
		
		Set<Integer> ids = new HashSet<Integer>();
		ids.addAll(query.list());
		
		return new Cohort("Batch of " + size + " patients starting at " + start, "", ids);
	}
	
}