/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.api.db.hibernate;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Conjunction;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.LogicalExpression;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.SimpleExpression;
import org.hibernate.type.StringType;
import org.openmrs.PatientIdentifierType;
import org.openmrs.api.AdministrationService;
import org.openmrs.api.context.Context;
import org.openmrs.util.OpenmrsConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The PatientSearchCriteria class. It has API to return a criteria from the Patient Name and
* identifier.
*
* @deprecated since 2.1.0 (in favor of Hibernate Search)
*/
@Deprecated
public class PatientSearchCriteria {
private final static Logger log = LoggerFactory.getLogger(PatientSearchCriteria.class);
private final SessionFactory sessionFactory;
private final Criteria criteria;
private PersonSearchCriteria personSearchCriteria;
/**
* @param sessionFactory
* @param criteria
*/
public PatientSearchCriteria(SessionFactory sessionFactory, Criteria criteria) {
this.sessionFactory = sessionFactory;
this.criteria = criteria;
this.personSearchCriteria = new PersonSearchCriteria();
}
/**
* Prepare a hibernate criteria for searching patients by name and/or identifier.
*
* The visibility of this method remains public in order not to break OpenMRS modules that use this method.
*
* Instead of calling this method consider using {@link org.openmrs.api.PatientService} or
* {@link org.openmrs.api.db.PatientDAO}.
*
* @param name
* @param identifier
* @param identifierTypes
* @param matchIdentifierExactly
* @param searchOnNamesOrIdentifiers specifies if the logic should find patients that match the
* name or identifier otherwise find patients that match both the name and identifier
* @return {@link Criteria}
*/
public Criteria prepareCriteria(String name, String identifier, List<PatientIdentifierType> identifierTypes,
boolean matchIdentifierExactly, boolean orderByNames, boolean searchOnNamesOrIdentifiers) {
PatientSearchMode patientSearchMode = getSearchMode(name, identifier, identifierTypes, searchOnNamesOrIdentifiers);
switch (patientSearchMode) {
case PATIENT_SEARCH_BY_NAME:
addAliasForName(criteria, orderByNames);
criteria.add(prepareCriterionForName(name));
break;
case PATIENT_SEARCH_BY_IDENTIFIER:
addAliasForIdentifiers(criteria);
criteria.add(prepareCriterionForIdentifier(identifier, identifierTypes, matchIdentifierExactly));
break;
case PATIENT_SEARCH_BY_NAME_OR_IDENTIFIER:
// If only name *or* identifier is provided as a search parameter,
// the respective value is copied to the empty search parameter.
//
// As a consequence, the *single* parameter is used to search for both names and identifiers.
//
name = copySearchParameter(identifier, name);
identifier = copySearchParameter(name, identifier);
addAliasForName(criteria, orderByNames);
addAliasForIdentifiers(criteria);
criteria.add(Restrictions.disjunction().add(prepareCriterionForName(name)).add(
prepareCriterionForIdentifier(identifier, identifierTypes, matchIdentifierExactly)));
break;
case PATIENT_SEARCH_BY_NAME_AND_IDENTIFIER:
addAliasForName(criteria, orderByNames);
addAliasForIdentifiers(criteria);
criteria.add(prepareCriterionForName(name));
criteria.add(prepareCriterionForIdentifier(identifier, identifierTypes, matchIdentifierExactly));
break;
default:
break;
}
criteria.add(Restrictions.eq("voided", false));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
log.debug(criteria.toString());
return criteria;
}
/**
* Provides a Hibernate criteria object for searching patients by name, identifier or searchable attribute.
*
* The visibility of this method is "default" as this method should NOT be called directly by classes other
* than org.openmrs.api.db.hibernate.HibernatePatientDAO.
*
* Instead of calling this method consider using {@link org.openmrs.api.PatientService} or
* {@link org.openmrs.api.db.PatientDAO}.
*
* @param query defines search parameters
* @param includeVoided true/false whether or not to included voided patients
* @return criteria for searching by name OR identifier OR searchable attributes
*/
Criteria prepareCriteria(String query, boolean includeVoided) {
addAliasForName(criteria, true);
personSearchCriteria.addAliasForAttribute(criteria);
addAliasForIdentifiers(criteria);
criteria.add(Restrictions.disjunction().add(prepareCriterionForName(query, includeVoided)).add(
prepareCriterionForAttribute(query, includeVoided)).add(
prepareCriterionForIdentifier(query, new ArrayList<PatientIdentifierType>(), false, includeVoided)));
if (!includeVoided) {
criteria.add(Restrictions.eq("voided", false));
}
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
log.debug(criteria.toString());
return criteria;
}
/**
* Provides a Hibernate criteria object for searching patients by name, identifier or searchable attribute.
*
* The visibility of this method is "default" as this method should NOT be called directly by classes other
* than org.openmrs.api.db.hibernate.HibernatePatientDAO.
*
* Instead of calling this method consider using {@link org.openmrs.api.PatientService} or
* {@link org.openmrs.api.db.PatientDAO}.
*
* @param query defines search parameters
* @return criteria for searching by name OR identifier OR searchable attributes
*/
Criteria prepareCriteria(String query) {
return prepareCriteria(query, false);
}
/**
* @param query defines search parameters
* @param matchExactly
* @param orderByNames
* @param includeVoided true/false whether or not to included voided patients
* @return criteria for searching by name OR identifier OR searchable attributes
*/
Criteria prepareCriteria(String query, Boolean matchExactly, boolean orderByNames, boolean includeVoided) {
addAliasForName(criteria, orderByNames);
if (matchExactly == null) {
criteria.add(Restrictions.conjunction().add(prepareCriterionForName(query, null, includeVoided)).add(
Restrictions.not(prepareCriterionForName(query, true, includeVoided))).add(
Restrictions.not(prepareCriterionForName(query, false, includeVoided))));
} else if (!matchExactly) {
criteria.add(prepareCriterionForName(query, false, includeVoided));
} else {
personSearchCriteria.addAliasForAttribute(criteria);
addAliasForIdentifiers(criteria);
criteria.add(Restrictions.disjunction().add(prepareCriterionForName(query, true, includeVoided)).add(
prepareCriterionForAttribute(query, includeVoided)).add(
prepareCriterionForIdentifier(query, new ArrayList<PatientIdentifierType>(), false, includeVoided)));
}
if (!includeVoided) {
criteria.add(Restrictions.eq("voided", false));
}
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
log.debug(criteria.toString());
return criteria;
}
/**
* @should return source value when target is blank
* @should return target value when target is non-blank
*/
String copySearchParameter(String source, String target) {
if (!StringUtils.isBlank(source) && StringUtils.isBlank(target)) {
return source;
}
return target;
}
/**
* @should identify search by name
* @should identify search by identifier
* @should identify search by identifier type list
* @should identify search by identifier and identifier type list
* @should identify search by name or identifier
* @should identify search by name and identifier
*/
PatientSearchMode getSearchMode(String name, String identifier, List<PatientIdentifierType> identifierTypes,
boolean searchOnNamesOrIdentifiers) {
if (searchOnNamesOrIdentifiers) {
return PatientSearchMode.PATIENT_SEARCH_BY_NAME_OR_IDENTIFIER;
}
if (!StringUtils.isBlank(name) && StringUtils.isBlank(identifier) && CollectionUtils.isEmpty(identifierTypes)) {
return PatientSearchMode.PATIENT_SEARCH_BY_NAME;
}
// de Morgan's law coming to fruition: (!A||!B) <=> !(A&&B)
//
if (StringUtils.isBlank(name) && !(StringUtils.isBlank(identifier) && CollectionUtils.isEmpty(identifierTypes))) {
return PatientSearchMode.PATIENT_SEARCH_BY_IDENTIFIER;
}
return PatientSearchMode.PATIENT_SEARCH_BY_NAME_AND_IDENTIFIER;
}
private void addAliasForName(Criteria criteria, boolean orderByNames) {
criteria.createAlias("names", "name");
if (orderByNames) {
criteria.addOrder(Order.asc("name.givenName"));
criteria.addOrder(Order.asc("name.middleName"));
criteria.addOrder(Order.asc("name.familyName"));
}
}
private void addAliasForIdentifiers(Criteria criteria) {
criteria.createAlias("identifiers", "ids", CriteriaSpecification.LEFT_JOIN);
}
/**
* Utility method to add identifier expression to an existing criteria
*
* @param identifier
* @param identifierTypes
* @param matchIdentifierExactly
* @param includeVoided true/false whether or not to included voided patients
*/
private Criterion prepareCriterionForIdentifier(String identifier, List<PatientIdentifierType> identifierTypes,
boolean matchIdentifierExactly, boolean includeVoided) {
identifier = HibernateUtil.escapeSqlWildcards(identifier, sessionFactory);
Conjunction conjunction = Restrictions.conjunction();
if (!includeVoided) {
conjunction.add(Restrictions.eq("ids.voided", false));
}
// do the identifier restriction
if (identifier != null) {
// if the user wants an exact search, match on that.
if (matchIdentifierExactly) {
SimpleExpression matchIdentifier = Restrictions.eq("ids.identifier", identifier);
if (Context.getAdministrationService().isDatabaseStringComparisonCaseSensitive()) {
matchIdentifier.ignoreCase();
}
conjunction.add(matchIdentifier);
} else {
AdministrationService adminService = Context.getAdministrationService();
String regex = adminService.getGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_IDENTIFIER_REGEX, "");
String patternSearch = adminService.getGlobalProperty(
OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_IDENTIFIER_SEARCH_PATTERN, "");
// remove padding from identifier search string
if (Pattern.matches("^\\^.{1}\\*.*$", regex)) {
identifier = removePadding(identifier, regex);
}
if (org.springframework.util.StringUtils.hasLength(patternSearch)) {
conjunction.add(splitAndGetSearchPattern(identifier, patternSearch));
}
// if the regex is empty, default to a simple "like" search or if
// we're in hsql world, also only do the simple like search (because
// hsql doesn't know how to deal with 'regexp'
else if ("".equals(regex) || HibernateUtil.isHSQLDialect(sessionFactory)) {
conjunction.add(getCriterionForSimpleSearch(identifier, adminService));
}
// if the regex is present, search on that
else {
regex = replaceSearchString(regex, identifier);
conjunction.add(Restrictions.sqlRestriction("identifier regexp ?", regex, StringType.INSTANCE));
}
}
}
// do the type restriction
if (!CollectionUtils.isEmpty(identifierTypes)) {
criteria.add(Restrictions.in("ids.identifierType", identifierTypes));
}
return conjunction;
}
/**
* Utility method to add identifier expression to an existing criteria
*
* @param identifier
* @param identifierTypes
* @param matchIdentifierExactly
*/
private Criterion prepareCriterionForIdentifier(String identifier, List<PatientIdentifierType> identifierTypes,
boolean matchIdentifierExactly) {
return prepareCriterionForIdentifier(identifier, identifierTypes, matchIdentifierExactly, false);
}
/**
* Utility method to add prefix and suffix like expression
*
* @param identifier
* @param adminService
*/
private Criterion getCriterionForSimpleSearch(String identifier, AdministrationService adminService) {
String prefix = adminService.getGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_IDENTIFIER_PREFIX, "");
String suffix = adminService.getGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_IDENTIFIER_SUFFIX, "");
StringBuilder likeString = new StringBuilder(prefix).append(identifier).append(suffix);
return Restrictions.ilike("ids.identifier", likeString.toString());
}
/**
* Utility method to add search pattern expression to identifier.
*
* @param identifier
* @param patternSearch
*/
private Criterion splitAndGetSearchPattern(String identifier, String patternSearch) {
// split the pattern before replacing in case the user searched on a comma
List<String> searchPatterns = new ArrayList<String>();
// replace the @SEARCH@, etc in all elements
for (String pattern : patternSearch.split(",")) {
searchPatterns.add(replaceSearchString(pattern, identifier));
}
return Restrictions.in("ids.identifier", searchPatterns);
}
/**
* Utility method to remove padding from the identifier.
*
* @param identifier
* @param regex
* @return identifier without the padding.
*/
private String removePadding(String identifier, String regex) {
String padding = regex.substring(regex.indexOf("^") + 1, regex.indexOf("*"));
Pattern pattern = Pattern.compile("^" + padding + "+");
identifier = pattern.matcher(identifier).replaceFirst("");
return identifier;
}
/**
* Utility method to add name expressions to criteria.
*
* @param name
* @param matchExactly
* @param includeVoided true/false whether or not to included voided patients
*/
private Criterion prepareCriterionForName(String name, Boolean matchExactly, boolean includeVoided) {
name = HibernateUtil.escapeSqlWildcards(name, sessionFactory);
Conjunction conjunction = Restrictions.conjunction();
String[] nameParts = getQueryParts(name);
if (nameParts.length > 0) {
StringBuilder multiName = new StringBuilder(nameParts[0]);
for (int i = 0; i < nameParts.length; i++) {
String singleName = nameParts[i];
if (singleName != null && singleName.length() > 0) {
Criterion singleNameCriterion = getCriterionForName(singleName, matchExactly, includeVoided);
Criterion criterion = singleNameCriterion;
if (i > 0) {
multiName.append(" ");
multiName.append(singleName);
Criterion multiNameCriterion = getCriterionForName(multiName.toString(), matchExactly, includeVoided);
criterion = Restrictions.or(singleNameCriterion, multiNameCriterion);
}
conjunction.add(criterion);
}
}
}
return conjunction;
}
/**
* Utility method to add name expressions to criteria.
*
* @param name
* @param includeVoided true/false whether or not to included voided patients
*/
private Criterion prepareCriterionForName(String name, boolean includeVoided) {
return prepareCriterionForName(name, null, includeVoided);
}
/**
* Utility method to add name expressions to criteria.
*
* @param name
*/
private Criterion prepareCriterionForName(String name) {
return prepareCriterionForName(name, null, false);
}
/**
* @should process simple space as separator
* @should process comma as separator
* @should process mixed separators
* @should not return empty name parts
* @should reject null as name
**/
String[] getQueryParts(String query) {
if (query == null) {
throw new IllegalArgumentException("query must not be null");
}
query = query.replace(",", " ");
String[] queryPartArray = query.split(" ");
List<String> queryPartList = new ArrayList<String>();
for (String queryPart : queryPartArray) {
if (queryPart.trim().length() > 0) {
queryPartList.add(queryPart);
}
}
return queryPartList.toArray(new String[0]);
}
/**
* Returns a criteria object comparing the given string to each part of the name. <br>
* <br>
* This criteria is essentially:
* <p>
*
* <pre>
* ... where voided = false && name in (familyName2, familyName, middleName, givenName)
* </pre>
*
* Except when the name provided is less than min characters (usually 3) then we will look for
* an EXACT match by default
*
* @param name
* @param matchExactly
* @param includeVoided true/false whether or not to included voided patients
* @return {@link LogicalExpression}
*/
private Criterion getCriterionForName(String name, Boolean matchExactly, boolean includeVoided) {
if (isShortName(name)) {
return getCriterionForShortName(name, includeVoided);
} else {
if (matchExactly != null) {
if (matchExactly) {
return getCriterionForShortName(name, includeVoided);
}
return getCriterionForNoExactName(name, includeVoided);
}
return getCriterionForLongName(name, includeVoided);
}
}
/**
* @should recognise short name
* @should recognise long name
*/
Boolean isShortName(String name) {
Integer minChars = Context.getAdministrationService().getGlobalPropertyValue(
OpenmrsConstants.GLOBAL_PROPERTY_MIN_SEARCH_CHARACTERS,
OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_MIN_SEARCH_CHARACTERS);
if (name != null && name.length() < minChars) {
return Boolean.TRUE;
} else {
return Boolean.FALSE;
}
}
private Criterion getCriterionForShortName(String name, boolean includeVoided) {
Criterion criterion = Restrictions.disjunction().add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.givenName")).add(
Restrictions.eq("name.givenName", name).ignoreCase())).add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.middleName")).add(
Restrictions.eq("name.middleName", name).ignoreCase())).add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.familyName")).add(
Restrictions.eq("name.familyName", name).ignoreCase())).add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.familyName2")).add(
Restrictions.eq("name.familyName2", name).ignoreCase()));
if (!includeVoided) {
return Restrictions.conjunction().add(Restrictions.eq("name.voided", false)).add(criterion);
}
return criterion;
}
private Criterion getCriterionForLongName(String name, boolean includeVoided) {
MatchMode matchMode = getMatchMode();
Criterion criterion = Restrictions.disjunction().add(Restrictions.like("name.givenName", name, matchMode)).add(
Restrictions.like("name.middleName", name, matchMode))
.add(Restrictions.like("name.familyName", name, matchMode)).add(
Restrictions.like("name.familyName2", name, matchMode));
if (!includeVoided) {
return Restrictions.conjunction().add(Restrictions.eq("name.voided", false)).add(criterion);
}
return criterion;
}
private Criterion getCriterionForNoExactName(String name, boolean includeVoided) {
MatchMode matchMode = getMatchMode();
Criterion criterion = Restrictions.conjunction().add(
Restrictions.disjunction().add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.givenName")).add(
Restrictions.like("name.givenName", name, matchMode))).add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.middleName")).add(
Restrictions.like("name.middleName", name, matchMode))).add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.familyName")).add(
Restrictions.like("name.familyName", name, matchMode))).add(
Restrictions.conjunction().add(Restrictions.isNotNull("name.familyName2")).add(
Restrictions.like("name.familyName2", name, matchMode)))).add(
Restrictions.disjunction().add(Restrictions.isNull("name.givenName")).add(
Restrictions.ne("name.givenName", name))).add(
Restrictions.disjunction().add(Restrictions.isNull("name.middleName")).add(
Restrictions.ne("name.middleName", name))).add(
Restrictions.disjunction().add(Restrictions.isNull("name.familyName")).add(
Restrictions.ne("name.familyName", name))).add(
Restrictions.disjunction().add(Restrictions.isNull("name.familyName2")).add(
Restrictions.ne("name.familyName2", name)));
if (!includeVoided) {
return Restrictions.conjunction().add(Restrictions.eq("name.voided", false)).add(criterion);
}
return criterion;
}
/**
* @should return start as default match mode
* @should return start as configured match mode
* @should return anywhere as configured match mode
*/
MatchMode getMatchMode() {
String matchMode = Context.getAdministrationService().getGlobalProperty(
OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_MODE,
OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_START);
if (matchMode.equalsIgnoreCase(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_ANYWHERE)) {
return MatchMode.ANYWHERE;
}
return MatchMode.START;
}
/**
* Puts @SEARCH@, @SEARCH-1@, and @CHECKDIGIT@ into the search string
*
* @param regex the admin-defined search string containing the @..@'s to be replaced
* @param identifierSearched the user entered search string
* @return substituted search strings.
*/
private String replaceSearchString(String regex, String identifierSearched) {
String returnString = regex.replaceAll("@SEARCH@", identifierSearched);
if (identifierSearched.length() > 1) {
// for 2 or more character searches, we allow regex to use last character as check digit
returnString = returnString.replaceAll("@SEARCH-1@", identifierSearched.substring(0,
identifierSearched.length() - 1));
returnString = returnString.replaceAll("@CHECKDIGIT@", identifierSearched
.substring(identifierSearched.length() - 1));
} else {
returnString = returnString.replaceAll("@SEARCH-1@", "");
returnString = returnString.replaceAll("@CHECKDIGIT@", "");
}
return returnString;
}
private Criterion prepareCriterionForAttribute(String query, boolean includeVoided) {
query = HibernateUtil.escapeSqlWildcards(query, sessionFactory);
Conjunction conjunction = Restrictions.conjunction();
MatchMode matchMode = personSearchCriteria.getAttributeMatchMode();
String[] queryParts = getQueryParts(query);
for (String queryPart : queryParts) {
conjunction.add(personSearchCriteria.prepareCriterionForAttribute(queryPart, includeVoided, matchMode));
}
return conjunction;
}
}