/* * 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; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.Vector; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.search.annotations.ContainedIn; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Index; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.IndexedEmbedded; import org.openmrs.api.APIException; import org.openmrs.api.ConceptNameType; import org.openmrs.api.ConceptService; import org.openmrs.api.context.Context; import org.openmrs.util.LocaleUtility; import org.openmrs.util.OpenmrsUtil; import org.simpleframework.xml.Attribute; import org.simpleframework.xml.Element; import org.simpleframework.xml.ElementList; import org.simpleframework.xml.Root; import org.springframework.util.ObjectUtils; /** * A Concept object can represent either a question or an answer to a data point. That data point is * usually an {@link Obs}. <br/> * <br/> * A Concept can have multiple names and multiple descriptions within one locale and across multiple * locales.<br/> * <br/> * To save a Concept to the database, first build up the Concept object in java, then pass that * object to the {@link ConceptService}.<br/> * <br/> * To get a Concept that is stored in the database, call a method in the {@link ConceptService} to * fetch an object. To get child objects off of that Concept, further calls to the * {@link ConceptService} or the database are not needed. e.g. To get the list of answers that are * stored to a concept, get the concept, then call {@link Concept#getAnswers()} * * @see ConceptName * @see ConceptDescription * @see ConceptAnswer * @see ConceptSet * @see ConceptMap * @see ConceptService */ @Root public class Concept extends BaseOpenmrsObject implements Auditable, Retireable, java.io.Serializable, Attributable<Concept> { public static final long serialVersionUID = 57332L; private static final Log log = LogFactory.getLog(Concept.class); // Fields // @DocumentId // @Field(index = Index.TOKENIZED) private Integer conceptId; // @Field(index = Index.TOKENIZED) private Boolean retired = false; // @Field(index = Index.TOKENIZED) private User retiredBy; // @Field(index = Index.TOKENIZED) private Date dateRetired; // @Field(index = Index.TOKENIZED) private String retireReason; // @Field(index = Index.TOKENIZED) private ConceptDatatype datatype; // @Field(index = Index.TOKENIZED) private ConceptClass conceptClass; // @Field private Boolean set = false; // @Field private String version; // @Field(index = Index.TOKENIZED) // @IndexedEmbedded private User creator; // @Field private Date dateCreated; // @Field(index = Index.TOKENIZED) // @IndexedEmbedded private User changedBy; // @Field(index = Index.TOKENIZED) private Date dateChanged; // @Field(index = Index.TOKENIZED) // @ContainedIn private Collection<ConceptName> names; // @Field(index = Index.TOKENIZED) // @ContainedIn private Collection<ConceptAnswer> answers; // @Field(index = Index.TOKENIZED) // @ContainedIn private Collection<ConceptSet> conceptSets; // @Field(index = Index.TOKENIZED) // @ContainedIn private Collection<ConceptDescription> descriptions; // @Field(index = Index.TOKENIZED) // @ContainedIn private Collection<ConceptMap> conceptMappings; /** * A cache of locales to names which have compatible locales. Built on-the-fly by * getCompatibleNames(). */ private Map<Locale, List<ConceptName>> compatibleCache; /** default constructor */ public Concept() { names = new HashSet<ConceptName>(); answers = new HashSet<ConceptAnswer>(); conceptSets = new TreeSet<ConceptSet>(); descriptions = new HashSet<ConceptDescription>(); conceptMappings = new HashSet<ConceptMap>(); } /** * Convenience constructor with conceptid to save to {@link #setConceptId(Integer)}. This * effectively creates a concept stub that can be used to make other calls. Because the * {@link #equals(Object)} and {@link #hashCode()} methods rely on conceptId, this allows a stub * to masquerade as a full concept as long as other objects like {@link #getAnswers()} and * {@link #getNames()} are not needed/called. * * @param conceptId the concept id to set */ public Concept(Integer conceptId) { this.conceptId = conceptId; } /** * Possibly used for decapitating a ConceptNumeric (to remove the row in * * @param cn * @deprecated */ @Deprecated public Concept(ConceptNumeric cn) { conceptId = cn.getConceptId(); retired = cn.isRetired(); datatype = cn.getDatatype(); conceptClass = cn.getConceptClass(); version = cn.getVersion(); creator = cn.getCreator(); dateCreated = cn.getDateCreated(); changedBy = cn.getChangedBy(); dateChanged = cn.getDateChanged(); names = cn.getNames(); descriptions = cn.getDescriptions(); answers = cn.getAnswers(true); conceptSets = cn.getConceptSets(); conceptMappings = cn.getConceptMappings(); } /** * @see java.lang.Object#equals(java.lang.Object) * @should not fail if given obj has null conceptid * @should not fail if given obj is null * @should not fail if concept id is null * @should confirm two new concept objects are equal */ @Override public boolean equals(Object obj) { if (obj == null) return false; if (obj instanceof Concept) { Concept c = (Concept) obj; if (getConceptId() == null && c.getConceptId() == null) return this == obj; if (getConceptId() != null) return (this.getConceptId().equals(c.getConceptId())); } return this == obj; } /** * @see java.lang.Object#hashCode() * @should not fail if concept id is null */ @Override public int hashCode() { if (this.getConceptId() == null) return super.hashCode(); int hash = 8; hash = 31 * this.getConceptId() + hash; return hash; } /** * @return Returns all answers (including retired answers). * @should return retired and non-retired answers * @should not return null if no answers defined */ @ElementList public Collection<ConceptAnswer> getAnswers() { return (answers != null) ? answers : new HashSet<ConceptAnswer>(); } /** * TODO describe use cases * * @param locale * @return the answers for this concept sorted according to ConceptAnswerComparator */ @Deprecated public Collection<ConceptAnswer> getSortedAnswers(Locale locale) { Vector<ConceptAnswer> sortedAnswers = new Vector<ConceptAnswer>(getAnswers()); Collections.sort(sortedAnswers); return sortedAnswers; } /** * If <code>includeRetired</code> is true, then the returned object is the actual stored list of * {@link ConceptAnswer}s (which may be null.) * * @param includeRetired true/false whether to also include the retired answers * @return Returns the answers for this Concept * @should return actual answers object if given includeRetired is true */ public Collection<ConceptAnswer> getAnswers(boolean includeRetired) { if (!includeRetired) { Collection<ConceptAnswer> newAnswers = new HashSet<ConceptAnswer>(); if (answers != null) { for (ConceptAnswer ca : answers) { if (!ca.getAnswerConcept().isRetired()) newAnswers.add(ca); } } return newAnswers; } else return answers; } /** * Set this Concept as having the given <code>answers</code>; This method assumes that the * sort_weight has already been set. * * @param answers The answers to set. */ @ElementList public void setAnswers(Collection<ConceptAnswer> answers) { this.answers = answers; } /** * Add the given ConceptAnswer to the list of answers for this Concept * * @param conceptAnswer * @should add the ConceptAnswer to Concept * @should not fail if answers list is null * @should not fail if answers contains ConceptAnswer already * @should set the sort weight to the max plus one if not provided */ public void addAnswer(ConceptAnswer conceptAnswer) { if (conceptAnswer != null) { if (getAnswers(true) == null) { answers = new HashSet<ConceptAnswer>(); conceptAnswer.setConcept(this); answers.add(conceptAnswer); } else if (!answers.contains(conceptAnswer)) { conceptAnswer.setConcept(this); answers.add(conceptAnswer); } if ((conceptAnswer.getSortWeight() == null) || (conceptAnswer.getSortWeight() <= 0)) { //find largest sort weight ConceptAnswer a = Collections.max(answers); Double sortWeight = (a == null) ? 1d : ((a.getSortWeight() == null) ? 1d : a.getSortWeight() + 1d);//a.sortWeight can be NULL conceptAnswer.setSortWeight(sortWeight); } } } /** * Remove the given answer from the list of answers for this Concept * * @param conceptAnswer answer to remove * @return true if the entity was removed, false otherwise * @should not fail if answers is empty * @should not fail if given answer does not exist in list */ public boolean removeAnswer(ConceptAnswer conceptAnswer) { if (getAnswers() != null) return answers.remove(conceptAnswer); else return false; } /** * @return Returns the changedBy. */ @Element(required = false) public User getChangedBy() { return changedBy; } /** * @param changedBy The changedBy to set. */ @Element(required = false) public void setChangedBy(User changedBy) { this.changedBy = changedBy; } /** * @return Returns the conceptClass. */ @Element public ConceptClass getConceptClass() { return conceptClass; } /** * @param conceptClass The conceptClass to set. */ @Element public void setConceptClass(ConceptClass conceptClass) { this.conceptClass = conceptClass; } /** * whether or not this concept is a set */ public Boolean isSet() { return set; } /** * @param set whether or not this concept is a set */ @Attribute public void setSet(Boolean set) { this.set = set; } @Attribute public Boolean getSet() { return isSet(); } /** * @return Returns the conceptDatatype. */ @Element public ConceptDatatype getDatatype() { return datatype; } /** * @param conceptDatatype The conceptDatatype to set. */ @Element public void setDatatype(ConceptDatatype conceptDatatype) { this.datatype = conceptDatatype; } /** * @return Returns the conceptId. */ @Attribute(required = true) public Integer getConceptId() { return conceptId; } /** * @param conceptId The conceptId to set. */ @Attribute(required = true) public void setConceptId(Integer conceptId) { this.conceptId = conceptId; } /** * @return Returns the creator. */ @Element public User getCreator() { return creator; } /** * @param creator The creator to set. */ @Element public void setCreator(User creator) { this.creator = creator; } /** * @return Returns the dateChanged. */ @Element(required = false) public Date getDateChanged() { return dateChanged; } /** * @param dateChanged The dateChanged to set. */ @Element(required = false) public void setDateChanged(Date dateChanged) { this.dateChanged = dateChanged; } /** * @return Returns the dateCreated. */ @Element public Date getDateCreated() { return dateCreated; } /** * @param dateCreated The dateCreated to set. */ @Element public void setDateCreated(Date dateCreated) { this.dateCreated = dateCreated; } /** * @deprecated use {@link #setPreferredName(ConceptName)} */ @Deprecated public void setPreferredName(Locale locale, ConceptName preferredName) { setPreferredName(preferredName); } /** * Sets the preferred name /in this locale/ to the specified conceptName and its Locale, if * there is an existing preferred name for this concept in the same locale, this one will * replace the old preferred name. Also, the name is added to the concept if it is not already * among the concept names. * * @param preferredName The name to be marked as preferred in its locale * @should only allow one preferred name * @should add the name to the list of names if it not among them before * @should fail if the preferred name to set to is an index term */ public void setPreferredName(ConceptName preferredName) { if (preferredName.getLocale() == null) throw new APIException("The locale for a concept name cannot be null"); else if (preferredName != null && !preferredName.isVoided() && !preferredName.isIndexTerm()) { //first revert the current preferred name(if any) from being preferred ConceptName oldPreferredName = getPreferredName(preferredName.getLocale()); if (oldPreferredName != null) oldPreferredName.setLocalePreferred(false); preferredName.setLocalePreferred(true); //add this name, if it is new or not among this concept's names if (preferredName.getConceptNameId() == null || !getNames().contains(preferredName)) addName(preferredName); } else throw new APIException("Preferred name cannot be null, voided or an index term"); } /** * Gets the name explicitly marked as preferred in a locale with a matching country code. * * @param country ISO-3166 two letter country code * @return the preferred name, or null if no match is found * @deprecated use {@link #getPreferredName(Locale)} */ @Deprecated public ConceptName getPreferredNameForCountry(String country) { //TODO add unit tests if (!StringUtils.isBlank(country)) { //return the first preferred name found in a locale with a matching country code for (ConceptName conceptName : getNames()) { if (conceptName.isPreferred() && conceptName.getLocale() != null && conceptName.getLocale().getCountry().equals(country)) return conceptName; } } return null; } /** * Gets the name explicitly marked as preferred in a locale with a matching language code. * * @param country ISO-3166 two letter language code * @return the preferred name, or null if no match is found * @deprecated use {@link #getPreferredName(Locale)} */ @Deprecated public ConceptName getPreferredNameInLanguage(String language) { //TODO add unit tests if (!StringUtils.isBlank(language)) { //return the first preferred name found in a locale with a matching language code for (ConceptName conceptName : getNames()) { if (conceptName.isPreferred() && conceptName.getLocale() != null && conceptName.getLocale().getLanguage().equals(language)) return conceptName; } } return null; } /** * A convenience method to get the concept-name (if any) which has a particular tag. This does * not guarantee that the returned name is the only one with the tag. * * @param conceptNameTag the tag for which to look * @return the tagged name, or null if no name has the tag */ public ConceptName findNameTaggedWith(ConceptNameTag conceptNameTag) { ConceptName taggedName = null; for (ConceptName possibleName : getNames()) { if (possibleName.hasTag(conceptNameTag)) { taggedName = possibleName; break; } } return taggedName; } /** * Returns a name in the given locale. If a name isn't found with an exact match, a compatible * locale match is returned. If no name is found matching either of those, the first name * defined for this concept is returned. * * @param locale the locale to fetch for * @return ConceptName attributed to the Concept in the given locale * @since 1.5 * @see Concept#getNames(Locale) to get all the names for a locale, * @see Concept#getPreferredName(Locale) for the preferred name (if any) */ public ConceptName getName(Locale locale) { return getName(locale, false); } /** * Returns concept name, the look up for the appropriate name is done in the following order; * <ul> * <li>First name found in any locale that is explicitly marked as preferred while searching * available locales in order of preference (the locales are traversed in their order as they * are listed in the 'locale.allowed.list' including english global property).</li> * <li>First "Fully Specified" name found while searching available locales in order of * preference.</li> * <li>The first fully specified name found while searching through all names for the concept</li> * <li>The first synonym found while searching through all names for the concept.</li> * <li>The first random name found(except index terms) while searching through all names.</li> * </ul> * * @return {@link ConceptName} in the current locale or any locale if none found * @since 1.5 * @see Concept#getNames(Locale) to get all the names for a locale * @see Concept#getPreferredName(Locale) for the preferred name (if any) * @should return the name explicitly marked as locale preferred if any is present * @should return the fully specified name in a locale if no preferred name is set * @should return null if the only added name is an index term * @should return name in broader locale incase none is found in specific one */ public ConceptName getName() { if (getNames().size() == 0) { if (log.isDebugEnabled()) log.debug("there are no names defined for: " + conceptId); return null; } for (Locale currentLocale : LocaleUtility.getLocalesInOrder()) { ConceptName preferredName = getPreferredName(currentLocale); if (preferredName != null) return preferredName; ConceptName fullySpecifiedName = getFullySpecifiedName(currentLocale); if (fullySpecifiedName != null) return fullySpecifiedName; //if the locale has an variants e.g en_GB, try names in the locale excluding the country code i.e en if (!StringUtils.isBlank(currentLocale.getCountry()) || !StringUtils.isBlank(currentLocale.getVariant())) { Locale broaderLocale = new Locale(currentLocale.getLanguage()); ConceptName prefNameInBroaderLoc = getPreferredName(broaderLocale); if (prefNameInBroaderLoc != null) return prefNameInBroaderLoc; ConceptName fullySpecNameInBroaderLoc = getFullySpecifiedName(broaderLocale); if (fullySpecNameInBroaderLoc != null) return fullySpecNameInBroaderLoc; } } for (ConceptName cn : getNames()) { if (cn.isFullySpecifiedName()) return cn; } if (getSynonyms().size() > 0) return getSynonyms().iterator().next(); //we dont expect to get here since every concept name must have atleast //one fully specified name, but just in case(probably inconsistent data) return null; } /** * Checks whether this concept has the given string in any of the names in the given locale * already. * * @param name the ConceptName.name to compare to * @param locale the locale to look in (null to check all locales) * @return true/false whether the name exists already */ public boolean hasName(String name, Locale locale) { if (name == null) return false; Collection<ConceptName> currentNames = null; if (locale == null) currentNames = getNames(); else currentNames = getNames(locale); for (ConceptName currentName : currentNames) { if (name.equals(currentName.getName())) return true; } return false; } /** * Returns a name in the given locale. If a name isn't found with an exact match, a compatible * locale match is returned. If no name is found matching either of those, the first name * defined for this concept is returned. * * @param locale the language and country in which the name is used * @param exact true/false to return only exact locale (no default locale) * @return the closest name in the given locale, or the first name * @see Concept#getNames(Locale) to get all the names for a locale, * @see Concept#getPreferredName(Locale) for the preferred name (if any) * @should return exact name locale match given exact equals true * @should return loose match given exact equals false * @should return null if no names are found in locale given exact equals true * @should return any name if no locale match given exact equals false */ public ConceptName getName(Locale locale, boolean exact) { // fail early if this concept has no names defined if (getNames().size() == 0) { if (log.isDebugEnabled()) log.debug("there are no names defined for: " + conceptId); return null; } if (log.isDebugEnabled()) log.debug("Getting conceptName for locale: " + locale); if (exact && locale != null) { ConceptName preferredName = getPreferredName(locale); if (preferredName != null) return preferredName; ConceptName fullySpecifiedName = getFullySpecifiedName(locale); if (fullySpecifiedName != null) return fullySpecifiedName; else if (getSynonyms(locale).size() > 0) return getSynonyms(locale).iterator().next(); return null; } else { //just get any name return getName(); } } /** * Returns the name which is explicitly marked as preferred for a given locale. * * @param forLocale locale for which to return a preferred name * @return preferred name for the locale, or null if no preferred name is specified * @should return the concept name explicitly marked as locale preferred * @should return the fully specified name if no name is explicitly marked as locale preferred */ public ConceptName getPreferredName(Locale forLocale) { if (log.isDebugEnabled()) log.debug("Getting preferred conceptName for locale: " + forLocale); // fail early if this concept has no names defined if (getNames(forLocale).size() == 0) { if (log.isDebugEnabled()) log.debug("there are no names defined for concept with id: " + conceptId + " in the locale: " + forLocale); return null; } else if (forLocale == null) { log.warn("Locale cannot be null"); return null; } for (ConceptName nameInLocale : getNames(forLocale)) { if (ObjectUtils.nullSafeEquals(nameInLocale.isLocalePreferred(), true)) return nameInLocale; } return getFullySpecifiedName(forLocale); } /** * @deprecated use {@link #getName(Locale, boolean)} with a second parameter of "false" */ @Deprecated public ConceptName getBestName(Locale locale) { return getName(locale, false); } /** * Convenience method that returns the fully specified name in the locale * * @param locale locale from which to look up the fully specified name * @return the name explicitly marked as fully specified for the locale * @should return the name marked as fully specified for the given locale */ public ConceptName getFullySpecifiedName(Locale locale) { if (locale != null && getNames(locale).size() > 0) { //get the first fully specified name, since every concept must have a fully specified name, //then, this loop will have to return a name for (ConceptName conceptName : getNames(locale)) { if (ObjectUtils.nullSafeEquals(conceptName.isFullySpecifiedName(), true)) return conceptName; } } return null; } /** * Returns all names available in a specific locale. <br/> * <br/> * This is recommended when managing the concept dictionary. * * @param locale locale for which names should be returned * @return Collection of ConceptNames with the given locale */ public Collection<ConceptName> getNames(Locale locale) { Collection<ConceptName> localeNames = new Vector<ConceptName>(); for (ConceptName possibleName : getNames()) { if (possibleName.getLocale().equals(locale)) { localeNames.add(possibleName); } } return localeNames; } /** * Returns all names from compatible locales. A locale is considered compatible if it is exactly * the same locale, or if either locale has no country specified and the language matches. <br/> * <br/> * This is recommended when presenting possible names to the use. * * @param desiredLocale locale with which the names should be compatible * @return Collection of compatible names * @should exclude incompatible country locales * @should exclude incompatible language locales */ public List<ConceptName> getCompatibleNames(Locale desiredLocale) { // lazy create the cache List<ConceptName> compatibleNames = null; if (compatibleCache == null) { compatibleCache = new HashMap<Locale, List<ConceptName>>(); } else { compatibleNames = compatibleCache.get(desiredLocale); } if (compatibleNames == null) { compatibleNames = new Vector<ConceptName>(); for (ConceptName possibleName : getNames()) { if (LocaleUtility.areCompatible(possibleName.getLocale(), desiredLocale)) { compatibleNames.add(possibleName); } } compatibleCache.put(desiredLocale, compatibleNames); } return compatibleNames; } /** * @deprecated use {@link #getShortNameInLocale(Locale)} or * {@link #getShortestName(Locale, Boolean)} */ @Deprecated public ConceptName getBestShortName(Locale locale) { return getShortestName(locale, false); } /** *@deprecated use {@link #setShortName(ConceptName)} */ @Deprecated public void setShortName(Locale locale, ConceptName shortName) { setShortName(shortName); } /** * Sets the specified name as the fully specified name for the locale and the current fully * specified (if any) ceases to be the fully specified name for the locale. * * @param newFullySpecifiedName the new fully specified name to set * @should set the concept name type of the specified name to fully specified * @should convert the previous fully specified name if any to a synonym * @should add the name to the list of names if it not among them before */ public void setFullySpecifiedName(ConceptName fullySpecifiedName) { if (fullySpecifiedName.getLocale() == null) throw new APIException("The locale for a concept name cannot be null"); else if (fullySpecifiedName != null && !fullySpecifiedName.isVoided()) { ConceptName oldFullySpecifiedName = getFullySpecifiedName(fullySpecifiedName.getLocale()); if (oldFullySpecifiedName != null) oldFullySpecifiedName.setConceptNameType(null); fullySpecifiedName.setConceptNameType(ConceptNameType.FULLY_SPECIFIED); //add this name, if it is new or not among this concept's names if (fullySpecifiedName.getConceptNameId() == null || !getNames().contains(fullySpecifiedName)) addName(fullySpecifiedName); } else throw new APIException("Fully Specified name cannot be null or voided"); } /** * Sets the specified name as the short name for the locale and the current shortName(if any) * ceases to be the short name for the locale. * * @param shortName the new shortName to set * @should set the concept name type of the specified name to short * @should convert the previous shortName if any to a synonym * @should add the name to the list of names if it not among them before */ public void setShortName(ConceptName shortName) { if (shortName.getLocale() == null) throw new APIException("The locale for a concept name cannot be null"); else if (shortName != null && !shortName.isVoided()) { ConceptName oldShortName = getShortNameInLocale(shortName.getLocale()); if (oldShortName != null) oldShortName.setConceptNameType(null); shortName.setConceptNameType(ConceptNameType.SHORT); //add this name, if it is new or not among this concept's names if (shortName.getConceptNameId() == null || !getNames().contains(shortName)) addName(shortName); } else throw new APIException("Short name cannot be null or voided"); } /** * This method is deprecated, it always returns the shortName from the locale with a matching * country code. * * @param country ISO-3166 two letter country code * @return the short name, or null if none has been explicitly set * @deprecated use {@link #getShortNameInLocale(Locale)} or * {@link #getShortestName(Locale, Boolean)} */ @Deprecated public ConceptName getShortNameForCountry(String country) { if (!StringUtils.isBlank(country)) { //return the first short name found in a locale with a matching country code for (ConceptName shortName : getShortNames()) { if (shortName.getLocale() != null && shortName.getLocale().getCountry().equals(country)) return shortName; } } return null; } /** * This method is deprecated, it always returns the shortName from the locale with a matching * language code. * * @param country ISO-3166 two letter language code * @return the short name, or null if none has been explicitly set * @deprecated use {@link #getShortNameInLocale(Locale)} or * {@link #getShortestName(Locale, Boolean)} */ @Deprecated public ConceptName getShortNameInLanguage(String language) { if (!StringUtils.isBlank(language)) { //return the first short name found in a locale with a matching language code for (ConceptName shortName : getShortNames()) { if (shortName.getLocale() != null && shortName.getLocale().getLanguage().equals(language)) return shortName; } } return null; } /** * Gets the explicitly specified short name for a locale. * * @param locale locale for which to find a short name * @return the short name, or null if none has been explicitly set */ public ConceptName getShortNameInLocale(Locale locale) { if (locale != null && getShortNames().size() > 0) { for (ConceptName shortName : getShortNames()) { if (shortName.getLocale().equals(locale)) return shortName; } } return null; } /** * Gets a collection of short names for this concept from all locales. * * @return a collection of all short names for this concept */ public Collection<ConceptName> getShortNames() { Vector<ConceptName> shortNames = new Vector<ConceptName>(); if (getNames().size() == 0) { if (log.isDebugEnabled()) log.debug("The Concept with id: " + conceptId + " has no names"); } else { for (ConceptName name : getNames()) { if (name.isShort()) shortNames.add(name); } } return shortNames; } /** * This method is deprecated, it returns a list with only one shortName for the locale if any is * found, otherwise the list will be empty. * * @param the locale where to find the shortName * @return a list containing a single shortName for the locale if any is found * @deprecated because each concept has only one short name per locale. * @see #getShortNameInLocale(Locale) */ @Deprecated public Collection<ConceptName> getShortNamesForLocale(Locale locale) { //return a list with only the single short name for the locale if any Vector<ConceptName> shortNamesForLocale = new Vector<ConceptName>(); ConceptName shortNameInLocale = getShortNameInLocale(locale); if (shortNameInLocale != null) shortNamesForLocale.add(shortNameInLocale); return shortNamesForLocale; } /** * Returns the short form name for a locale, or if none has been identified, the shortest name * available in the locale. If exact is false, the shortest name from any locale is returned * * @param locale the language and country in which the short name is used * @param exact true/false to return only exact locale (no default locale) * @return the appropriate short name, or null if not found * @should return the name marked as the shortName for the locale if it is present * @should return the shortest name in a given locale for a concept if exact is true * @should return the shortest name for the concept from any locale if exact is false * @should return null if their are no names in the specified locale and exact is true */ public ConceptName getShortestName(Locale locale, Boolean exact) { if (log.isDebugEnabled()) log.debug("Getting shortest conceptName for locale: " + locale); ConceptName shortNameInLocale = getShortNameInLocale(locale); if (shortNameInLocale != null) return shortNameInLocale; ConceptName shortestNameForLocale = null; ConceptName shortestNameForConcept = null; if (locale != null) { for (Iterator<ConceptName> i = getNames().iterator(); i.hasNext();) { ConceptName possibleName = i.next(); if (possibleName.getLocale().equals(locale)) { if ((shortestNameForLocale == null) || (possibleName.getName().length() < shortestNameForLocale.getName().length())) { shortestNameForLocale = possibleName; } } if ((shortestNameForConcept == null) || (possibleName.getName().length() < shortestNameForConcept.getName().length())) { shortestNameForConcept = possibleName; } } } if (exact) { if (shortestNameForLocale == null) log.warn("No short concept name found for concept id " + conceptId + " for locale " + locale.getDisplayName()); return shortestNameForLocale; } return shortestNameForConcept; } /** * @param name A name * @return whether this concept has the given name in any locale */ public boolean isNamed(String name) { for (ConceptName cn : getNames()) if (name.equals(cn.getName())) return true; return false; } /** * Gets the list of all non-retired concept names which are index terms for this concept * * @return a collection of concept names which are index terms for this concept * @since 1.7 */ public Collection<ConceptName> getIndexTerms() { Collection<ConceptName> indexTerms = new Vector<ConceptName>(); for (ConceptName name : getNames()) { if (name.isIndexTerm()) indexTerms.add(name); } return indexTerms; } /** * Gets the list of all non-retired concept names which are index terms in a given locale * * @param locale the locale for the index terms to return * @return a collection of concept names which are index terms in the given locale * @since 1.7 */ public Collection<ConceptName> getIndexTermsForLocale(Locale locale) { Vector<ConceptName> indexTermsForLocale = new Vector<ConceptName>(); if (getIndexTerms().size() > 0) { for (ConceptName name : getIndexTerms()) { if (name.getLocale().equals(locale)) indexTermsForLocale.add(name); } } return indexTermsForLocale; } /** * @return Returns the names. */ @ElementList public Collection<ConceptName> getNames() { return getNames(false); } /** * @return Returns the names. * @param includeVoided Include voided ConceptNames if true. */ public Collection<ConceptName> getNames(boolean includeVoided) { Collection<ConceptName> ret = new HashSet<ConceptName>(); if (includeVoided) { if (names != null) return names; else return ret; } else { if (names != null) { for (ConceptName cn : names) { if (!cn.isVoided()) ret.add(cn); } } return ret; } } /** * @param names The names to set. */ @ElementList public void setNames(Collection<ConceptName> names) { this.names = names; } /** * Add the given ConceptName to the list of names for this Concept * * @param conceptName * @should replace the old preferred name with a current one * @should replace the old fully specified name with a current one * @should replace the old short name with a current one * @should mark the first name added as fully specified */ public void addName(ConceptName conceptName) { if (conceptName != null) { conceptName.setConcept(this); if (names == null) names = new HashSet<ConceptName>(); if (conceptName != null && !names.contains(conceptName)) { if (getNames().size() == 0 && !OpenmrsUtil.nullSafeEquals(conceptName.getConceptNameType(), ConceptNameType.FULLY_SPECIFIED)) { conceptName.setConceptNameType(ConceptNameType.FULLY_SPECIFIED); } else { if (conceptName.isPreferred() && !conceptName.isIndexTerm() && conceptName.getLocale() != null) { ConceptName prefName = getPreferredName(conceptName.getLocale()); if (prefName != null) prefName.setLocalePreferred(false); } if (conceptName.isFullySpecifiedName() && conceptName.getLocale() != null) { ConceptName fullySpecName = getFullySpecifiedName(conceptName.getLocale()); if (fullySpecName != null) fullySpecName.setConceptNameType(null); } else if (conceptName.isShort() && conceptName.getLocale() != null) { ConceptName shortName = getShortNameInLocale(conceptName.getLocale()); if (shortName != null) shortName.setConceptNameType(null); } } names.add(conceptName); if (compatibleCache != null) { compatibleCache.clear(); // clear the locale cache, forcing it to be rebuilt } } } } /** * Remove the given name from the list of names for this Concept * * @param conceptName * @return true if the entity was removed, false otherwise */ public boolean removeName(ConceptName conceptName) { if (names != null) return names.remove(conceptName); else return false; } /** * Finds the description of the concept using the current locale in Context.getLocale(). Returns * null if none found. * * @return ConceptDescription attributed to the Concept in the given locale */ public ConceptDescription getDescription() { return getDescription(Context.getLocale()); } /** * Finds the description of the concept in the given locale. Returns null if none found. * * @param locale * @return ConceptDescription attributed to the Concept in the given locale */ public ConceptDescription getDescription(Locale locale) { return getDescription(locale, false); } /** * Returns the preferred description for a locale. * * @param locale the language and country in which the description is used * @param exact true/false to return only exact locale (no default locale) * @return the appropriate description, or null if not found * @should return match on locale exactly * @should return match on language only * @should not return match on language only if exact match exists * @should not return language only match for exact matches */ public ConceptDescription getDescription(Locale locale, boolean exact) { log.debug("Getting ConceptDescription for locale: " + locale); ConceptDescription foundDescription = null; if (locale == null) locale = LocaleUtility.getDefaultLocale(); Locale desiredLocale = locale; ConceptDescription defaultDescription = null; for (Iterator<ConceptDescription> i = getDescriptions().iterator(); i.hasNext();) { ConceptDescription availableDescription = i.next(); Locale availableLocale = availableDescription.getLocale(); if (availableLocale.equals(desiredLocale)) { foundDescription = availableDescription; break; // skip out now because we found an exact locale match } if (!exact && LocaleUtility.areCompatible(availableLocale, desiredLocale)) foundDescription = availableDescription; if (availableLocale.equals(LocaleUtility.getDefaultLocale())) defaultDescription = availableDescription; } if (foundDescription == null) { // no description with the given locale was found. // return null if exact match desired if (exact) { log.debug("No concept description found for concept id " + conceptId + " for locale " + desiredLocale.toString()); } else { // returning default description locale ("en") if exact match // not desired if (defaultDescription == null) log.debug("No concept description found for default locale for concept id " + conceptId); else { foundDescription = defaultDescription; } } } return foundDescription; } /** * @return the retiredBy */ public User getRetiredBy() { return retiredBy; } /** * @param retiredBy the retiredBy to set */ public void setRetiredBy(User retiredBy) { this.retiredBy = retiredBy; } /** * @return the dateRetired */ public Date getDateRetired() { return dateRetired; } /** * @param dateRetired the dateRetired to set */ public void setDateRetired(Date dateRetired) { this.dateRetired = dateRetired; } /** * @return the retireReason */ public String getRetireReason() { return retireReason; } /** * @param retireReason the retireReason to set */ public void setRetireReason(String retireReason) { this.retireReason = retireReason; } /** * @return Returns the descriptions. */ @ElementList public Collection<ConceptDescription> getDescriptions() { return descriptions; } /** * Sets the collection of descriptions for this Concept. * * @param descriptions the collection of descriptions */ @ElementList public void setDescriptions(Collection<ConceptDescription> descriptions) { this.descriptions = descriptions; } /** * Add the given description to the list of descriptions for this Concept * * @param description the description to add */ public void addDescription(ConceptDescription description) { if (description != null) { if (getDescriptions() == null) { descriptions = new HashSet<ConceptDescription>(); description.setConcept(this); descriptions.add(description); } else if (!descriptions.contains(description)) { description.setConcept(this); descriptions.add(description); } } } /** * Remove the given description from the list of descriptions for this Concept * * @param description the description to remove * @return true if the entity was removed, false otherwise */ public boolean removeDescription(ConceptDescription description) { if (getDescriptions() != null) return descriptions.remove(description); else return false; } /** * @return Returns the retired. */ public Boolean isRetired() { return retired; } /** * This method exists to satisfy spring and hibernates slightly bung use of Boolean object * getters and setters. * * @deprecated Use the "proper" isRetired method. * @see org.openmrs.Concept#isRetired() */ @Deprecated @Attribute public Boolean getRetired() { return isRetired(); } /** * @param retired The retired to set. */ @Attribute public void setRetired(Boolean retired) { this.retired = retired; } /** * Gets the synonyms in the given locale. Returns a list of names from the same language, or an * empty list if none found. * * @param locale * @return Collection of ConceptNames which are synonyms for the Concept in the given locale */ public Collection<ConceptName> getSynonyms(Locale locale) { Collection<ConceptName> syns = new Vector<ConceptName>(); for (ConceptName possibleSynonymInLoc : getSynonyms()) { if (locale.equals(possibleSynonymInLoc.getLocale())) syns.add(possibleSynonymInLoc); } log.debug("returning: " + syns); return syns; } /** * Gets all the non-retired synonyms. * * @return Collection of ConceptNames which are synonyms for the Concept or an empty list if * none is found * @since 1.7 */ public Collection<ConceptName> getSynonyms() { Collection<ConceptName> synonyms = new Vector<ConceptName>(); for (ConceptName possibleSynonym : getNames()) { if (possibleSynonym.isSynonym()) { synonyms.add(possibleSynonym); } } log.debug("returning: " + synonyms); return synonyms; } /** * @return Returns the version. */ @Attribute(required = false) public String getVersion() { return version; } /** * @param version The version to set. */ @Attribute(required = false) public void setVersion(String version) { this.version = version; } /** * @return Returns the conceptSets. */ @ElementList(required = false) public Collection<ConceptSet> getConceptSets() { return conceptSets; } /** * @param conceptSets The conceptSets to set. */ @ElementList(required = false) public void setConceptSets(Collection<ConceptSet> conceptSets) { this.conceptSets = conceptSets; } /** * Whether this concept is numeric or not. This will <i>always</i> return false for concept * objects. ConceptNumeric.isNumeric() will then <i>always</i> return true. * * @return false */ public boolean isNumeric() { return false; } /** * @return the conceptMappings for this concept */ @ElementList(required = false) public Collection<ConceptMap> getConceptMappings() { return conceptMappings; } /** * @param conceptMappings the conceptMappings to set */ @ElementList(required = false) public void setConceptMappings(Collection<ConceptMap> conceptMappings) { this.conceptMappings = conceptMappings; } /** * Add the given ConceptMap object to this concept's list of concept mappings. If there is * already a corresponding ConceptMap object for this concept already, this one will not be * added. * * @param newConceptMap */ public void addConceptMapping(ConceptMap newConceptMap) { newConceptMap.setConcept(this); if (getConceptMappings() == null) conceptMappings = new HashSet<ConceptMap>(); if (newConceptMap != null && !conceptMappings.contains(newConceptMap)) conceptMappings.add(newConceptMap); } /** * Child Class ConceptComplex overrides this method and returns true. See * {@link org.openmrs.ConceptComplex#isComplex()}. Otherwise this method returns false. * * @return false * @since 1.5 */ public boolean isComplex() { return false; } /** * Remove the given ConceptMap from the list of mappings for this Concept * * @param conceptMap * @return true if the entity was removed, false otherwise */ public boolean removeConceptMapping(ConceptMap conceptMap) { if (getConceptMappings() != null) return conceptMappings.remove(conceptMap); else return false; } /** * @see java.lang.Object#toString() */ @Override public String toString() { if (conceptId == null) return ""; return conceptId.toString(); } /** * @see org.openmrs.Attributable#findPossibleValues(java.lang.String) */ public List<Concept> findPossibleValues(String searchText) { List<Concept> concepts = new Vector<Concept>(); try { for (ConceptSearchResult searchResult : Context.getConceptService().getConcepts(searchText, Collections.singletonList(Context.getLocale()), false, null, null, null, null, null, null, null)) { concepts.add(searchResult.getConcept()); } } catch (Exception e) { // pass } return concepts; } /** * @see org.openmrs.Attributable#getPossibleValues() */ public List<Concept> getPossibleValues() { try { return Context.getConceptService().getConceptsByName(""); } catch (Exception e) { // pass } return Collections.emptyList(); } /** * @see org.openmrs.Attributable#hydrate(java.lang.String) */ public Concept hydrate(String s) { try { return Context.getConceptService().getConcept(Integer.valueOf(s)); } catch (Exception e) { // pass } return null; } /** * Turns this concept into a very very simple serialized string * * @see org.openmrs.Attributable#serialize() */ public String serialize() { if (this.getConceptId() == null) return ""; return "" + this.getConceptId(); } /** * @see org.openmrs.Attributable#getDisplayString() */ public String getDisplayString() { if (getName() == null) return toString(); else return getName().getName(); } /** * Convenience method that returns a set of all the locales in which names have been added for * this concept. * * @return a set of all locales for names for this concept * @since 1.7 * @should return all locales for conceptNames for this concept without duplicates */ public Set<Locale> getAllConceptNameLocales() { if (getNames().size() == 0) { if (log.isDebugEnabled()) log.debug("The Concept with id: " + conceptId + " has no names"); return null; } Set<Locale> locales = new HashSet<Locale>(); for (ConceptName cn : getNames()) { locales.add(cn.getLocale()); } return locales; } /** * @since 1.5 * @see org.openmrs.OpenmrsObject#getId() */ public Integer getId() { return getConceptId(); } /** * @since 1.5 * @see org.openmrs.OpenmrsObject#setId(java.lang.Integer) */ public void setId(Integer id) { setConceptId(id); } /** * Sort the ConceptSet based on the weight * * @return sortedConceptSet Collection<ConceptSet> */ private List<ConceptSet> getSortedConceptSets() { List<ConceptSet> cs = new Vector<ConceptSet>(); if (conceptSets != null) { cs.addAll(conceptSets); Collections.sort(cs); } return cs; } /** * Get all the concept members of current concept * * @since 1.7 * @return List<Concept> the Concepts that are members of this Concept's set * @should return concept set members sorted according to the sort weight * @should return all the conceptMembers of current Concept * @should return unmodifiable list of conceptMember list */ public List<Concept> getSetMembers() { List<Concept> conceptMembers = new Vector<Concept>(); Collection<ConceptSet> sortedConceptSet = getSortedConceptSets(); for (ConceptSet conceptSet : sortedConceptSet) { conceptMembers.add(conceptSet.getConcept()); } return Collections.unmodifiableList(conceptMembers); } /** * Appends the concept to the end of the existing list of concept members for this Concept * * @since 1.7 * @param setMember Concept to add to the * @should add concept as a conceptSet * @should append concept to the existing list of conceptSet * @should place the new concept last in the list * @should assign the calling component as parent to the ConceptSet */ public void addSetMember(Concept setMember) { addSetMember(setMember, -1); } /** * Add the concept to the existing member to the list of set members in the given location. <br/> * <br/> * index of 0 is before the first concept<br/> * index of -1 is after last.<br/> * index of 1 is after the first but before the second, etc<br/> * * @param setMember the Concept to add as a child of this Concept * @param index where in the list of set members to put this setMember * @since 1.7 * @should assign the given concept as a ConceptSet * @should insert the concept before the first with zero index * @should insert the concept at the end with negative one index * @should insert the concept in the third slot * @should assign the calling component as parent to the ConceptSet * @should add the concept to the current list of conceptSet * @see #getSortedConceptSets() */ public void addSetMember(Concept setMember, int index) { List<ConceptSet> sortedConceptSets = getSortedConceptSets(); int setsSize = sortedConceptSets.size(); double weight; if (sortedConceptSets.isEmpty()) weight = 1000.0; else if (index == -1 || index >= setsSize) // deals with list size of 1 and any large index given by dev weight = sortedConceptSets.get(setsSize - 1).getSortWeight() + 10.0; else if (index == 0) weight = sortedConceptSets.get(0).getSortWeight() - 10.0; else { // put the weight between two double prevSortWeight = sortedConceptSets.get(index - 1).getSortWeight(); double nextSortWeight = sortedConceptSets.get(index).getSortWeight(); weight = (prevSortWeight + nextSortWeight) / 2; } ConceptSet conceptSet = new ConceptSet(setMember, weight); conceptSet.setConceptSet(this); conceptSets.add(conceptSet); } }