/** * 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.web.dwr; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Vector; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.Concept; import org.openmrs.ConceptAnswer; import org.openmrs.ConceptClass; import org.openmrs.ConceptDatatype; import org.openmrs.ConceptDescription; import org.openmrs.ConceptName; import org.openmrs.ConceptNumeric; import org.openmrs.ConceptSearchResult; import org.openmrs.ConceptSet; import org.openmrs.Drug; import org.openmrs.Field; import org.openmrs.User; import org.openmrs.api.APIException; import org.openmrs.api.ConceptService; import org.openmrs.api.ConceptsLockedException; import org.openmrs.api.FormService; import org.openmrs.api.context.Context; import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.OpenmrsUtil; /** * This class exposes some of the methods in org.openmrs.api.ConceptService via the dwr package */ public class DWRConceptService { protected static final Log log = LogFactory.getLog(DWRConceptService.class); /** * Gets a list of conceptListItems matching the given arguments * * @param phrase the concept name string to match against * @param includeRetired boolean if false, will exclude retired concepts * @param includeClassNames List of ConceptClasses to restrict to * @param excludeClassNames List of ConceptClasses to leave out of results * @param includeDatatypeNames List of ConceptDatatypes to restrict to * @param excludeDatatypeNames List of ConceptDatatypes to leave out of results * @param includeDrugConcepts Specifies if drugs with matching conceptNames should be included * @return a list of conceptListItems matching the given arguments */ public List<Object> findConcepts(String phrase, boolean includeRetired, List<String> includeClassNames, List<String> excludeClassNames, List<String> includeDatatypeNames, List<String> excludeDatatypeNames, boolean includeDrugConcepts) { return findBatchOfConcepts(phrase, includeRetired, includeClassNames, excludeClassNames, includeDatatypeNames, excludeDatatypeNames, null, null); } /** * Gets a list of conceptListItems matching the given arguments * * @param phrase the concept name string to match against * @param includeRetired boolean if false, will exclude retired concepts * @param includeClassNames List of ConceptClasses to restrict to * @param excludeClassNames List of ConceptClasses to leave out of results * @param includeDatatypeNames List of ConceptDatatypes to restrict to * @param excludeDatatypeNames List of ConceptDatatypes to leave out of results * @param start the beginning index * @param length the number of matching concepts to return * @return a list of conceptListItems matching the given arguments * @since 1.8 */ public List<Object> findBatchOfConcepts(String phrase, boolean includeRetired, List<String> includeClassNames, List<String> excludeClassNames, List<String> includeDatatypeNames, List<String> excludeDatatypeNames, Integer start, Integer length) { //TODO factor out the reusable code in this and findCountAndConcepts methods to a single utility method // List to return // Object type gives ability to return error strings Vector<Object> objectList = new Vector<Object>(); // TODO add localization for messages User currentUser = Context.getAuthenticatedUser(); Locale defaultLocale = Context.getLocale(); // get the list of locales to search on from the user's // defined proficient locales (if applicable) List<Locale> localesToSearchOn = null; if (currentUser != null) localesToSearchOn = currentUser.getProficientLocales(); if (localesToSearchOn == null) // we're working with an anonymous user right now or // with a user that has not defined any proficient locales localesToSearchOn = new Vector<Locale>(); // add the user's locale if (localesToSearchOn.size() == 0) { localesToSearchOn.add(defaultLocale); // if country is specified, also add the generic language locale if (!"".equals(defaultLocale.getCountry())) { localesToSearchOn.add(new Locale(defaultLocale.getLanguage())); } } // debugging output if (log.isDebugEnabled()) { StringBuffer searchLocalesString = new StringBuffer(); for (Locale loc : localesToSearchOn) { searchLocalesString.append(loc.toString() + " "); } log.debug("searching locales: " + searchLocalesString); } if (includeClassNames == null) includeClassNames = new Vector<String>(); if (excludeClassNames == null) excludeClassNames = new Vector<String>(); if (includeDatatypeNames == null) includeDatatypeNames = new Vector<String>(); if (excludeDatatypeNames == null) excludeDatatypeNames = new Vector<String>(); try { ConceptService cs = Context.getConceptService(); List<ConceptSearchResult> searchResults = new Vector<ConceptSearchResult>(); if (phrase.matches("\\d+")) { // user searched on a number. Insert concept with // corresponding conceptId Concept c = cs.getConcept(Integer.valueOf(phrase)); if (c != null && (!c.isRetired() || includeRetired)) { ConceptName cn = c.getName(defaultLocale); ConceptSearchResult searchResult = new ConceptSearchResult(phrase, c, cn); searchResults.add(searchResult); } } if (!StringUtils.isBlank(phrase)) { // turn classnames into class objects List<ConceptClass> includeClasses = new Vector<ConceptClass>(); for (String name : includeClassNames) if (!"".equals(name)) includeClasses.add(cs.getConceptClassByName(name)); // turn classnames into class objects List<ConceptClass> excludeClasses = new Vector<ConceptClass>(); for (String name : excludeClassNames) if (!"".equals(name)) excludeClasses.add(cs.getConceptClassByName(name)); // turn classnames into class objects List<ConceptDatatype> includeDatatypes = new Vector<ConceptDatatype>(); for (String name : includeDatatypeNames) if (!"".equals(name)) includeDatatypes.add(cs.getConceptDatatypeByName(name)); // turn classnames into class objects List<ConceptDatatype> excludeDatatypes = new Vector<ConceptDatatype>(); for (String name : excludeDatatypeNames) if (!"".equals(name)) excludeDatatypes.add(cs.getConceptDatatypeByName(name)); // perform the search searchResults.addAll(cs.getConcepts(phrase, localesToSearchOn, includeRetired, includeClasses, excludeClasses, includeDatatypes, excludeDatatypes, null, start, length)); //TODO Should we still include drugs, if yes, smartly harmonize the paging between the two different DB tables //look ups to match the values of start and length not to go over the value of count of matches returned to the search widget //List<Drug> drugs = null; //if (includeDrugConcepts) // drugs = cs.getDrugs(phrase, null, false, includeRetired, null, null); } if (searchResults.size() < 1) { objectList.add(Context.getMessageSourceService() .getMessage("general.noMatchesFoundInLocale", new Object[] { "<b>" + phrase + "</b>", OpenmrsUtil.join(localesToSearchOn, ", ") }, Context.getLocale())); } else { // turn searchResults into concept list items // if user wants drug concepts included, append those for (ConceptSearchResult searchResult : searchResults) objectList.add(new ConceptListItem(searchResult)); } } catch (Exception e) { log.error("Error while finding concepts + " + e.getMessage(), e); objectList.add(Context.getMessageSourceService().getMessage("Concept.search.error") + " - " + e.getMessage()); } if (objectList.size() == 0) objectList.add(Context.getMessageSourceService().getMessage("general.noMatchesFoundInLocale", new Object[] { "<b>" + phrase + "</b>", defaultLocale }, Context.getLocale())); return objectList; } /** * Get a {@link ConceptListItem} by its internal database id. * * @param conceptId the id to look for * @return a {@link ConceptListItem} or null if conceptId is not found */ public ConceptListItem getConcept(Integer conceptId) { Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); Concept c = cs.getConcept(conceptId); if (c == null) return null; ConceptName cn = c.getName(locale); return new ConceptListItem(c, cn, locale); } public List<ConceptListItem> findProposedConcepts(String text) { Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); List<Concept> concepts = cs.getProposedConcepts(text); List<ConceptListItem> cli = new Vector<ConceptListItem>(); for (Concept c : concepts) { ConceptName cn = c.getName(locale); cli.add(new ConceptListItem(c, cn, locale)); } return cli; } /** * Find a list of {@link ConceptListItem} or {@link ConceptDrugListItem}s that are answers to * the given question. The given question is determined by the given <code>conceptId</code> * * @param text the text to search for within the answers * @param conceptId the conceptId of the question concept * @param includeVoided (this argument is ignored now. searching for voided answers is not * logical) * @param includeDrugConcepts if true, drug concepts are searched too * @return list of {@link ConceptListItem} or {@link ConceptDrugListItem} answers that match the * query * @throws Exception if given conceptId is not found */ public List<Object> findConceptAnswers(String text, Integer conceptId, boolean includeVoided, boolean includeDrugConcepts) throws Exception { if (includeVoided == true) throw new APIException("You should not include voideds in the search."); Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); Concept concept = cs.getConcept(conceptId); if (concept == null) throw new Exception("Unable to find a concept with id: " + conceptId); List<ConceptSearchResult> searchResults = cs.findConceptAnswers(text, locale, concept); List<Drug> drugAnswers = new Vector<Drug>(); for (ConceptAnswer conceptAnswer : concept.getAnswers(false)) { if (conceptAnswer.getAnswerDrug() != null) drugAnswers.add(conceptAnswer.getAnswerDrug()); } List<Object> items = new Vector<Object>(); for (ConceptSearchResult searchResult : searchResults) { items.add(new ConceptListItem(searchResult)); // add drugs for concept if desired if (includeDrugConcepts) { Integer classId = searchResult.getConcept().getConceptClass().getConceptClassId(); if (classId.equals(OpenmrsConstants.CONCEPT_CLASS_DRUG)) for (Drug d : cs.getDrugsByConcept(searchResult.getConcept())) { if (drugAnswers.contains(d)) items.add(new ConceptDrugListItem(d, locale)); } } } return items; } public List<Object> getConceptSet(Integer conceptId) { Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); FormService fs = Context.getFormService(); Concept concept = cs.getConcept(conceptId); List<Object> returnList = new Vector<Object>(); if (concept.isSet()) { for (ConceptSet set : concept.getConceptSets()) { Field field = null; ConceptName cn = set.getConcept().getName(locale); ConceptDescription description = set.getConcept().getDescription(locale); for (Field f : fs.getFieldsByConcept(set.getConcept())) { if (f.getName().equals(cn.getName()) && f.getDescription().equals(description.getDescription()) && f.isSelectMultiple().equals(false)) field = f; } if (field == null) returnList.add(new ConceptListItem(set.getConcept(), cn, locale)); else returnList.add(new FieldListItem(field, locale)); } } return returnList; } public List<ConceptListItem> getQuestionsForAnswer(Integer conceptId) { Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); Concept concept = cs.getConcept(conceptId); List<Concept> concepts = cs.getConceptsByAnswer(concept); List<ConceptListItem> items = new Vector<ConceptListItem>(); for (Concept c : concepts) { ConceptName cn = c.getName(locale); items.add(new ConceptListItem(c, cn, locale)); } return items; } public ConceptDrugListItem getDrug(Integer drugId) { Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); Drug d = cs.getDrug(drugId); return d == null ? null : new ConceptDrugListItem(d, locale); } public List<Object> getDrugs(Integer conceptId, boolean showConcept) { Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); Concept concept = cs.getConcept(conceptId); List<Object> items = new Vector<Object>(); // Add this concept as the first option in the list // If there are no drugs to choose from, this will be automatically // selected // by the openmrsSearch.fillTable(objs) function if (showConcept == true) { ConceptDrugListItem thisConcept = new ConceptDrugListItem(null, conceptId, concept.getName(locale, false) .getName()); items.add(thisConcept); } // find drugs for this concept List<Drug> drugs = null; // if there are drugs to choose from, add some instructions if (drugs.size() > 0 && showConcept == true) items.add("Or choose a form of " + concept.getName(locale, false).getName()); // miniaturize our drug objects for (Drug drug : drugs) { items.add(new ConceptDrugListItem(drug, locale)); } return items; } public List<Object> findDrugs(String phrase, boolean includeRetired) throws APIException { if (includeRetired == true) throw new APIException("You should not include voideds in the search."); Locale locale = Context.getLocale(); ConceptService cs = Context.getConceptService(); List<Object> items = new Vector<Object>(); // find drugs for this concept List<Drug> drugs = cs.getDrugs(phrase); // miniaturize our drug objects for (Drug drug : drugs) { items.add(new ConceptDrugListItem(drug, locale)); } return items; } public boolean isValidNumericValue(Float value, Integer conceptId) { ConceptNumeric conceptNumeric = Context.getConceptService().getConceptNumeric(conceptId); return OpenmrsUtil.isValidNumericValue(value, conceptNumeric); } public String getConceptNumericUnits(Integer conceptId) { ConceptNumeric conceptNumeric = Context.getConceptService().getConceptNumeric(conceptId); return conceptNumeric.getUnits(); } public List<ConceptListItem> getAnswersForQuestion(Integer conceptId) { Vector<ConceptListItem> ret = new Vector<ConceptListItem>(); Concept c = Context.getConceptService().getConcept(conceptId); Collection<ConceptAnswer> answers = c.getAnswers(false); // TODO: deal with concept answers (e.g. drug) whose answer concept is null. (Not sure if this actually ever happens) Locale locale = Context.getLocale(); for (ConceptAnswer ca : answers) if (ca.getAnswerConcept() != null) { ConceptName cn = ca.getAnswerConcept().getName(locale); ret.add(new ConceptListItem(ca.getAnswerConcept(), cn, locale)); } return ret; } /** * Converts the datatype of a concept that already has Obs referencing it from boolean to coded * to support addition of more coded answers * * @param conceptId the conceptId of the concept to be converted * @return String to act as a signal if successfully converted or an error message */ public String convertBooleanConceptToCoded(Integer conceptId) { try { Context.getConceptService().convertBooleanConceptToCoded(Context.getConceptService().getConcept(conceptId)); //this particular message isn't displayed in the browser rather it acts as //a signal that the concept was successfully converted and should refresh page. return "refresh"; } catch (ConceptsLockedException cle) { log.error("Tried to save/convert concept while concepts were locked", cle); return Context.getMessageSourceService().getMessage("Concept.concepts.locked"); } catch (APIException e) { log.error("Error while trying to change the datatype of concept", e); return Context.getMessageSourceService().getMessage("Concept.cannot.save"); } } /** * Returns a map of results with the values as count of matches and a partial list of the * matching concepts (depending on values of start and length parameters) while the keys are are * 'count' and 'objectList' respectively, if the length parameter is not specified, then all * matches will be returned from the start index if specified. * * @param phrase concept name or conceptId * @param includeRetired boolean if false, will exclude retired concepts * @param includeClassNames List of ConceptClasses to restrict to * @param excludeClassNames List of ConceptClasses to leave out of results * @param includeDatatypeNames List of ConceptDatatypes to restrict to * @param excludeDatatypeNames List of ConceptDatatypes to leave out of results * @param start the beginning index * @param length the number of matching concepts to return * @param getMatchCount Specifies if the count of matches should be included in the returned map * @return a map of results * @throws APIException * @since 1.8 */ public Map<String, Object> findCountAndConcepts(String phrase, boolean includeRetired, List<String> includeClassNames, List<String> excludeClassNames, List<String> includeDatatypeNames, List<String> excludeDatatypeNames, Integer start, Integer length, boolean getMatchCount) throws APIException { //Map to return Map<String, Object> resultsMap = new HashMap<String, Object>(); Vector<Object> objectList = new Vector<Object>(); User currentUser = Context.getAuthenticatedUser(); Locale defaultLocale = Context.getLocale(); // get the list of locales to search on from the user's // defined proficient locales (if applicable) List<Locale> localesToSearchOn = null; if (currentUser != null) localesToSearchOn = currentUser.getProficientLocales(); if (localesToSearchOn == null) // we're working with an anonymous user right now or // with a user that has not defined any proficient locales localesToSearchOn = new Vector<Locale>(); // add the user's locale if (localesToSearchOn.size() == 0) { localesToSearchOn.add(defaultLocale); // if country is specified, also add the generic language locale if (!"".equals(defaultLocale.getCountry())) { localesToSearchOn.add(new Locale(defaultLocale.getLanguage())); } } // debugging output if (log.isDebugEnabled()) { StringBuffer searchLocalesString = new StringBuffer(); for (Locale loc : localesToSearchOn) { searchLocalesString.append(loc.toString() + " "); } log.debug("searching locales: " + searchLocalesString); } if (includeClassNames == null) includeClassNames = new Vector<String>(); if (excludeClassNames == null) excludeClassNames = new Vector<String>(); if (includeDatatypeNames == null) includeDatatypeNames = new Vector<String>(); if (excludeDatatypeNames == null) excludeDatatypeNames = new Vector<String>(); try { ConceptService cs = Context.getConceptService(); if (!StringUtils.isBlank(phrase)) { // turn classnames into class objects List<ConceptClass> includeClasses = new Vector<ConceptClass>(); for (String name : includeClassNames) if (!"".equals(name)) includeClasses.add(cs.getConceptClassByName(name)); // turn classnames into class objects List<ConceptClass> excludeClasses = new Vector<ConceptClass>(); for (String name : excludeClassNames) if (!"".equals(name)) excludeClasses.add(cs.getConceptClassByName(name)); // turn classnames into class objects List<ConceptDatatype> includeDatatypes = new Vector<ConceptDatatype>(); for (String name : includeDatatypeNames) if (!"".equals(name)) includeDatatypes.add(cs.getConceptDatatypeByName(name)); // turn classnames into class objects List<ConceptDatatype> excludeDatatypes = new Vector<ConceptDatatype>(); for (String name : excludeDatatypeNames) if (!"".equals(name)) excludeDatatypes.add(cs.getConceptDatatypeByName(name)); int matchCount = 0; if (getMatchCount) { //get the count of matches matchCount += cs.getCountOfConcepts(phrase, localesToSearchOn, includeRetired, includeClasses, excludeClasses, includeDatatypes, excludeDatatypes, null); if (phrase.matches("\\d+")) { // user searched on a number. Insert concept with // corresponding conceptId Concept c = cs.getConcept(Integer.valueOf(phrase)); if (c != null && (!c.isRetired() || includeRetired)) matchCount++; } //if (includeDrugs) // matchCount += cs.getCountOfDrugs(phrase, null, false, includeRetired); } //if we have any matches or this isn't the first ajax call when the caller //requests for the count if (matchCount > 0 || !getMatchCount) { objectList.addAll(findBatchOfConcepts(phrase, includeRetired, includeClassNames, excludeClassNames, includeDatatypeNames, excludeDatatypeNames, start, length)); } resultsMap.put("count", matchCount); resultsMap.put("objectList", objectList); } else { resultsMap.put("count", 0); objectList.add(Context.getMessageSourceService().getMessage("searchWidget.noMatchesFound")); } } catch (Exception e) { log.error("Error while searching for concepts", e); objectList.clear(); objectList.add(Context.getMessageSourceService().getMessage("Concept.search.error") + " - " + e.getMessage()); resultsMap.put("count", 0); resultsMap.put("objectList", objectList); } return resultsMap; } }