org.openmrs18.Concept.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs18.Concept.java

Source

/**
 * 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.openmrs18;

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.openmrs.Attributable;
import org.openmrs.Auditable;
import org.openmrs.BaseOpenmrsObject;
import org.openmrs.Obs;
import org.openmrs.Retireable;
import org.openmrs.User;
import org.openmrs.api.APIException;
import org.openmrs.api.ConceptService;
import org.openmrs.api.context.Context;
import org.openmrs.util.LocaleUtility;
import org.openmrs.util.OpenmrsUtil;
import org.openmrs18.api.ConceptNameType;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Root;

/**
 * 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

    private Integer conceptId;

    private Boolean retired = false;

    private User retiredBy;

    private Date dateRetired;

    private String retireReason;

    private ConceptDatatype datatype;

    private ConceptClass conceptClass;

    private Boolean set = false;

    private String version;

    private User creator;

    private Date dateCreated;

    private User changedBy;

    private Date dateChanged;

    private Collection<ConceptName> names;

    private Collection<ConceptAnswer> answers;

    private Collection<ConceptSet> conceptSets;

    private Collection<ConceptDescription> descriptions;

    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 concept_numeric)
     * 
     * @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();
        setUuid(cn.getUuid());
    }

    /**
     * @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 the non-retired answers.
     * @should not return retired answers
     * @should not return null if no answers defined
     */
    @ElementList
    public Collection<ConceptAnswer> getAnswers() {
        Collection<ConceptAnswer> newAnswers = new HashSet<ConceptAnswer>();
        if (answers != null) {
            for (ConceptAnswer ca : answers) {
                if (!ca.getAnswerConcept().isRetired())
                    newAnswers.add(ca);
            }
        }
        return newAnswers;
    }

    /**
     * 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)
            return getAnswers();
        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 : Context.getAdministrationService().getAllowedLocales()) {
            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 (nameInLocale.isLocalePreferred())
                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 (conceptName.isFullySpecifiedName())
                    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 Collections.emptyList();
    }

    /**
     * @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);
    }

}