• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 32520822383

21 Aug 2026 07:49PM UTC coverage: 64.501% (-0.02%) from 64.52%
32520822383

push

github

ibacher
TRUNK-6709: Remove internal copy amplification in Concept name resolution (#6365)

Co-authored-by: Ian <ian.c.bacher@gmail.com>

87 of 121 new or added lines in 1 file covered. (71.9%)

15 existing lines in 5 files now uncovered.

24813 of 38469 relevant lines covered (64.5%)

0.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.94
/api/src/main/java/org/openmrs/Concept.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs;
11

12
import javax.persistence.Cacheable;
13
import java.io.Serializable;
14
import java.util.ArrayList;
15
import java.util.Collection;
16
import java.util.Collections;
17
import java.util.Date;
18
import java.util.HashMap;
19
import java.util.HashSet;
20
import java.util.LinkedHashSet;
21
import java.util.List;
22
import java.util.Locale;
23
import java.util.Map;
24
import java.util.Set;
25
import java.util.TreeSet;
26
import java.util.stream.Collectors;
27

28
import org.apache.commons.lang3.StringUtils;
29
import org.codehaus.jackson.annotate.JsonIgnore;
30
import org.hibernate.annotations.Cache;
31
import org.hibernate.annotations.CacheConcurrencyStrategy;
32
import org.hibernate.envers.Audited;
33
import org.hibernate.search.engine.backend.types.ObjectStructure;
34
import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBridgeRef;
35
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.AssociationInverseSide;
36
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.DocumentId;
37
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
38
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
39
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded;
40
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;
41
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath;
42
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue;
43
import org.openmrs.annotation.AllowDirectAccess;
44
import org.openmrs.api.APIException;
45
import org.openmrs.api.ConceptNameType;
46
import org.openmrs.api.ConceptService;
47
import org.openmrs.api.context.Context;
48
import org.openmrs.api.db.hibernate.search.bridge.OpenmrsObjectValueBridge;
49
import org.openmrs.customdatatype.CustomValueDescriptor;
50
import org.openmrs.customdatatype.Customizable;
51
import org.openmrs.util.LocaleUtility;
52
import org.openmrs.util.OpenmrsUtil;
53
import org.slf4j.Logger;
54
import org.slf4j.LoggerFactory;
55
import org.springframework.util.ObjectUtils;
56

57
/**
58
 * A Concept object can represent either a question or an answer to a data point. That data point is
59
 * usually an {@link Obs}. <br>
60
 * <br>
61
 * A Concept can have multiple names and multiple descriptions within one locale and across multiple
62
 * locales.<br>
63
 * <br>
64
 * To save a Concept to the database, first build up the Concept object in java, then pass that
65
 * object to the {@link ConceptService}.<br>
66
 * <br>
67
 * To get a Concept that is stored in the database, call a method in the {@link ConceptService} to
68
 * fetch an object. To get child objects off of that Concept, further calls to the
69
 * {@link ConceptService} or the database are not needed. e.g. To get the list of answers that are
70
 * stored to a concept, get the concept, then call {@link Concept#getAnswers()}
71
 * 
72
 * @see ConceptName
73
 * @see ConceptDescription
74
 * @see ConceptAnswer
75
 * @see ConceptSet
76
 * @see ConceptMap
77
 * @see ConceptService
78
 */
79
@Cacheable
80
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
81
@Audited
82
public class Concept extends BaseOpenmrsObject implements Auditable, Retireable, Serializable, Attributable<Concept>,Customizable<ConceptAttribute> {
83
        
84
        public static final long serialVersionUID = 57332L;
85
        
86
        private static final Logger log = LoggerFactory.getLogger(Concept.class);
1✔
87
        private static final String CONCEPT_NAME_LOCALE_NULL = "Concept.name.locale.null";
88
        
89
        // Fields
90
        @DocumentId
91
        private Integer conceptId;
92
        
93
        @GenericField
1✔
94
        private Boolean retired = false;
1✔
95
        
96
        private User retiredBy;
97
        
98
        private Date dateRetired;
99
        
100
        private String retireReason;
101
        
102
        @KeywordField(
103
                valueBridge = @ValueBridgeRef(type = OpenmrsObjectValueBridge.class)
104
        )
105
        private ConceptDatatype datatype;
106

107
        @KeywordField(
108
                valueBridge = @ValueBridgeRef(type = OpenmrsObjectValueBridge.class)
109
        )
110
        private ConceptClass conceptClass;
111
        
112
        private Boolean set = false;
1✔
113
        
114
        private String version;
115
        
116
        private User creator;
117
        
118
        private Date dateCreated;
119
        
120
        private User changedBy;
121
        
122
        private Date dateChanged;
123
        
124
        @AllowDirectAccess
125
        @AssociationInverseSide(inversePath = @ObjectPath({@PropertyValue(propertyName = "concept")}))
126
        private Collection<ConceptName> names;
127
        
128
        @AllowDirectAccess
129
        private Collection<ConceptAnswer> answers;
130
        
131
        private Collection<ConceptSet> conceptSets;
132
        
133
        private Collection<ConceptDescription> descriptions;
134
        
135
        @IndexedEmbedded
136
        @AssociationInverseSide(inversePath = @ObjectPath({
137
                @PropertyValue(propertyName = "concept")
138
        }))
139
        private Collection<ConceptMap> conceptMappings;
140
        
141
        /**
142
         * A cache of locales to names which have compatible locales. Built on-the-fly by
143
         * getCompatibleNames().
144
         */
145
        private Map<Locale, List<ConceptName>> compatibleCache;
146

147
        private Set<ConceptAttribute> attributes = new LinkedHashSet<>();
1✔
148

149
        /** default constructor */
150
        public Concept() {
1✔
151
                names = new HashSet<>();
1✔
152
                answers = new HashSet<>();
1✔
153
                conceptSets = new TreeSet<>();
1✔
154
                descriptions = new HashSet<>();
1✔
155
                conceptMappings = new HashSet<>();
1✔
156
        }
1✔
157
        
158
        /**
159
         * Convenience constructor with conceptid to save to {@link #setConceptId(Integer)}. This
160
         * effectively creates a concept stub that can be used to make other calls. Because the
161
         * {@link #equals(Object)} and {@link #hashCode()} methods rely on conceptId, this allows a stub
162
         * to masquerade as a full concept as long as other objects like {@link #getAnswers()} and
163
         * {@link #getNames()} are not needed/called.
164
         * 
165
         * @param conceptId the concept id to set
166
         */
167
        public Concept(Integer conceptId) {
168
                this();
1✔
169
                this.conceptId = conceptId;
1✔
170
        }
1✔
171
        
172
        /**
173
         * @return Returns all answers (including retired answers).
174
         * <strong>Should</strong> return retired and non-retired answers
175
         * <strong>Should</strong> not return null if answers is null or empty
176
         */
177
        public Collection<ConceptAnswer> getAnswers() {
178
                if (answers == null) {
1✔
179
                        answers = new HashSet<>();
1✔
180
                }
181
                return answers;
1✔
182
        }
183
        
184
        /**
185
         * If <code>includeRetired</code> is true, then the returned object is the actual stored list of
186
         * {@link ConceptAnswer}s
187
         * 
188
         * @param includeRetired true/false whether to also include the retired answers
189
         * @return Returns the answers for this Concept
190
         * <strong>Should</strong> return the same as getAnswers() if includeRetired is true
191
         * <strong>Should</strong> not return retired answers if includeRetired is false
192
         */
193
        public Collection<ConceptAnswer> getAnswers(boolean includeRetired) {
194
                if (includeRetired) {
1✔
195
                        return getAnswers();
1✔
196
                } else {
197
                        return getAnswers().stream()
1✔
198
                                        .filter(a -> !a.getAnswerConcept().getRetired())
1✔
199
                                        .collect(Collectors.toSet());
1✔
200
                }
201
        }
202

203
        /**
204
         * Set this Concept as having the given <code>answers</code>; This method assumes that the
205
         * sort_weight has already been set.
206
         * 
207
         * @param answers The answers to set.
208
         */
209
        public void setAnswers(Collection<ConceptAnswer> answers) {
210
                this.answers = answers;
1✔
211
        }
1✔
212
        
213
        /**
214
         * Add the given ConceptAnswer to the list of answers for this Concept
215
         * 
216
         * @param conceptAnswer
217
         * <strong>Should</strong> add the ConceptAnswer to Concept
218
         * <strong>Should</strong> not fail if answers list is null
219
         * <strong>Should</strong> not fail if answers contains ConceptAnswer already
220
         * <strong>Should</strong> set the sort weight to the max plus one if not provided
221
         */
222
        public void addAnswer(ConceptAnswer conceptAnswer) {
223
                if (conceptAnswer != null) {
1✔
224
                        if (!getAnswers().contains(conceptAnswer)) {
1✔
225
                                conceptAnswer.setConcept(this);
1✔
226
                                getAnswers().add(conceptAnswer);
1✔
227
                        }
228
                        
229
                        if ((conceptAnswer.getSortWeight() == null) || (conceptAnswer.getSortWeight() <= 0)) {
1✔
230
                                //find largest sort weight
231
                                ConceptAnswer a = Collections.max(answers);
1✔
232
                                //a.sortWeight can be NULL
233
                                Double sortWeight = (a == null) ? 1d : ((a.getSortWeight() == null) ? 1d : a.getSortWeight() + 1d);
1✔
234
                                conceptAnswer.setSortWeight(sortWeight);
1✔
235
                        }
236
                }
237
        }
1✔
238
        
239
        /**
240
         * Remove the given answer from the list of answers for this Concept
241
         * 
242
         * @param conceptAnswer answer to remove
243
         * @return true if the entity was removed, false otherwise
244
         * <strong>Should</strong> not fail if answers is empty
245
         * <strong>Should</strong> not fail if given answer does not exist in list
246
         */
247
        public boolean removeAnswer(ConceptAnswer conceptAnswer) {
248
                return getAnswers().remove(conceptAnswer);
1✔
249
        }
250
        
251
        /**
252
         * @return Returns the changedBy.
253
         */
254
        @Override
255
        public User getChangedBy() {
256
                return changedBy;
1✔
257
        }
258
        
259
        /**
260
         * @param changedBy The changedBy to set.
261
         */
262
        @Override
263
        public void setChangedBy(User changedBy) {
264
                this.changedBy = changedBy;
1✔
265
        }
1✔
266
        
267
        /**
268
         * @return Returns the conceptClass.
269
         */
270
        public ConceptClass getConceptClass() {
271
                return conceptClass;
1✔
272
        }
273
        
274
        /**
275
         * @param conceptClass The conceptClass to set.
276
         */
277
        public void setConceptClass(ConceptClass conceptClass) {
278
                this.conceptClass = conceptClass;
1✔
279
        }
1✔
280
        
281
        /**
282
         * whether or not this concept is a set
283
         * 
284
         * @deprecated as of 2.0, use {@link #getSet()}
285
         */
286
        @Deprecated
287
        @JsonIgnore
288
        public Boolean isSet() {
289
                return getSet();
×
290
        }
291
        
292
        /**
293
         * @param set whether or not this concept is a set
294
         */
295
        public void setSet(Boolean set) {
296
                this.set = set;
1✔
297
        }
1✔
298
        
299
        public Boolean getSet() {
300
                return set;
1✔
301
        }
302
        
303
        /**
304
         * @return Returns the conceptDatatype.
305
         */
306
        public ConceptDatatype getDatatype() {
307
                return datatype;
1✔
308
        }
309
        
310
        /**
311
         * @param conceptDatatype The conceptDatatype to set.
312
         */
313
        public void setDatatype(ConceptDatatype conceptDatatype) {
314
                this.datatype = conceptDatatype;
1✔
315
        }
1✔
316
        
317
        /**
318
         * @return Returns the conceptId.
319
         */
320
        public Integer getConceptId() {
321
                return conceptId;
1✔
322
        }
323
        
324
        /**
325
         * @param conceptId The conceptId to set.
326
         */
327
        public void setConceptId(Integer conceptId) {
328
                this.conceptId = conceptId;
1✔
329
        }
1✔
330
        
331
        /**
332
         * @return Returns the creator.
333
         */
334
        @Override
335
        public User getCreator() {
336
                return creator;
1✔
337
        }
338
        
339
        /**
340
         * @param creator The creator to set.
341
         */
342
        @Override
343
        public void setCreator(User creator) {
344
                this.creator = creator;
1✔
345
        }
1✔
346
        
347
        /**
348
         * @return Returns the dateChanged.
349
         */
350
        @Override
351
        public Date getDateChanged() {
352
                return dateChanged;
1✔
353
        }
354
        
355
        /**
356
         * @param dateChanged The dateChanged to set.
357
         */
358
        @Override
359
        public void setDateChanged(Date dateChanged) {
360
                this.dateChanged = dateChanged;
1✔
361
        }
1✔
362
        
363
        /**
364
         * @return Returns the dateCreated.
365
         */
366
        @Override
367
        public Date getDateCreated() {
368
                return dateCreated;
1✔
369
        }
370
        
371
        /**
372
         * @param dateCreated The dateCreated to set.
373
         */
374
        @Override
375
        public void setDateCreated(Date dateCreated) {
376
                this.dateCreated = dateCreated;
1✔
377
        }
1✔
378
        
379
        /**
380
         * Sets the preferred name /in this locale/ to the specified conceptName and its Locale, if
381
         * there is an existing preferred name for this concept in the same locale, this one will
382
         * replace the old preferred name. Also, the name is added to the concept if it is not already
383
         * among the concept names.
384
         * 
385
         * @param preferredName The name to be marked as preferred in its locale
386
         * <strong>Should</strong> only allow one preferred name
387
         * <strong>Should</strong> add the name to the list of names if it not among them before
388
         * <strong>Should</strong> fail if the preferred name to set to is an index term
389
         */
390
        public void setPreferredName(ConceptName preferredName) {
391
                
392
                if (preferredName == null || preferredName.getVoided() || preferredName.isIndexTerm()) {
1✔
393
                        throw new APIException("Concept.error.preferredName.null", (Object[]) null);
1✔
394
                } else if (preferredName.getLocale() == null) {
1✔
395
                        throw new APIException(CONCEPT_NAME_LOCALE_NULL, (Object[]) null);
×
396
                }
397
                
398
                //first revert the current preferred name(if any) from being preferred
399
                ConceptName oldPreferredName = getPreferredName(preferredName.getLocale(), true);
1✔
400
                if (oldPreferredName != null) {
1✔
401
                        oldPreferredName.setLocalePreferred(false);
1✔
402
                }
403
                
404
                preferredName.setLocalePreferred(true);
1✔
405
                //add this name, if it is new or not among this concept's names
406
                if (preferredName.getConceptNameId() == null || !containsNonVoided(preferredName)) {
1✔
407
                        addName(preferredName);
1✔
408
                }
409
        }
1✔
410
        
411
        /**
412
         * A convenience method to get the concept-name (if any) which has a particular tag. This does
413
         * not guarantee that the returned name is the only one with the tag.
414
         * 
415
         * @param conceptNameTag the tag for which to look
416
         * @return the tagged name, or null if no name has the tag
417
         */
418
        public ConceptName findNameTaggedWith(ConceptNameTag conceptNameTag) {
NEW
419
                if (names == null) {
×
NEW
420
                        return null;
×
421
                }
NEW
422
                for (ConceptName possibleName : names) {
×
NEW
423
                        if (!possibleName.getVoided() && possibleName.hasTag(conceptNameTag)) {
×
NEW
424
                                return possibleName;
×
425
                        }
426
                }
×
NEW
427
                return null;
×
428
        }
429
        
430
        /**
431
         * Returns a name in the given locale. If a name isn't found with an exact match, a compatible
432
         * locale match is returned. If no name is found matching either of those, the first name
433
         * defined for this concept is returned.
434
         * 
435
         * @param locale the locale to fetch for
436
         * @return ConceptName attributed to the Concept in the given locale
437
         * @since 1.5
438
         * @see Concept#getNames(Locale) to get all the names for a locale,
439
         * @see Concept#getPreferredName(Locale) for the preferred name (if any)
440
         */
441
        public ConceptName getName(Locale locale) {
442
                return getName(locale, false);
1✔
443
        }
444
        
445
        /**
446
         * Returns concept name, the look up for the appropriate name is done in the following order;
447
         * <ul>
448
         * <li>First name found in any locale that is explicitly marked as preferred while searching
449
         * available locales in order of preference (the locales are traversed in their order as they
450
         * are listed in the 'locale.allowed.list' including english global property).</li>
451
         * <li>First "Fully Specified" name found while searching available locales in order of
452
         * preference.</li>
453
         * <li>The first fully specified name found while searching through all names for the concept</li>
454
         * <li>The first synonym found while searching through all names for the concept.</li>
455
         * <li>The first random name found(except index terms) while searching through all names.</li>
456
         * </ul>
457
         * 
458
         * @return {@link ConceptName} in the current locale or any locale if none found
459
         * @since 1.5
460
         * @see Concept#getNames(Locale) to get all the names for a locale
461
         * @see Concept#getPreferredName(Locale) for the preferred name (if any)
462
         * <strong>Should</strong> return the name explicitly marked as locale preferred if any is present
463
         * <strong>Should</strong> return the fully specified name in a locale if no preferred name is set
464
         * <strong>Should</strong> return null if the only added name is an index term
465
         * <strong>Should</strong> return name in broader locale in case none is found in specific one
466
         */
467
        public ConceptName getName() {
468
                if (!hasNonVoidedNames()) {
1✔
469
                        log.debug("there are no names defined for: {}", conceptId);
1✔
470
                        return null;
1✔
471
                }
472
                
473
                for (Locale currentLocale : LocaleUtility.getLocalesInOrder()) {
1✔
474
                        ConceptName preferredName = getPreferredName(currentLocale);
1✔
475
                        if (preferredName != null) {
1✔
476
                                return preferredName;
1✔
477
                        }
478
                        
479
                        ConceptName fullySpecifiedName = getFullySpecifiedName(currentLocale);
1✔
480
                        if (fullySpecifiedName != null) {
1✔
481
                                return fullySpecifiedName;
×
482
                        }
483
                        
484
                        //if the locale has an variants e.g en_GB, try names in the locale excluding the country code i.e en
485
                        if (!StringUtils.isBlank(currentLocale.getCountry()) || !StringUtils.isBlank(currentLocale.getVariant())) {
1✔
486
                                Locale broaderLocale = new Locale(currentLocale.getLanguage());
1✔
487
                                ConceptName prefNameInBroaderLoc = getPreferredName(broaderLocale);
1✔
488
                                if (prefNameInBroaderLoc != null) {
1✔
489
                                        return prefNameInBroaderLoc;
1✔
490
                                }
491
                                
492
                                ConceptName fullySpecNameInBroaderLoc = getFullySpecifiedName(broaderLocale);
1✔
493
                                if (fullySpecNameInBroaderLoc != null) {
1✔
494
                                        return fullySpecNameInBroaderLoc;
×
495
                                }
496
                        }
497
                }
1✔
498

499
                for (ConceptName cn : names) {
1✔
500
                        if (!cn.getVoided() && cn.isFullySpecifiedName()) {
1✔
501
                                return cn;
1✔
502
                        }
503
                }
1✔
504

505
                for (ConceptName cn : names) {
1✔
506
                        if (!cn.getVoided() && cn.isSynonym()) {
1✔
NEW
507
                                return cn;
×
508
                        }
509
                }
1✔
510
                
511
                // we don't expect to get here since every concept name must have at least
512
                // one fully specified name, but just in case (probably inconsistent data)
513
                
514
                return null;
1✔
515
        }
516
        
517
        /**
518
         * Checks whether this concept has the given string in any of the names in the given locale
519
         * already.
520
         * 
521
         * @param name the ConceptName.name to compare to
522
         * @param locale the locale to look in (null to check all locales)
523
         * @return true/false whether the name exists already
524
         * <strong>Should</strong> return false if name is null
525
         * <strong>Should</strong> return true if locale is null but name exists
526
         * <strong>Should</strong> return false if locale is null but name does not exist
527
         */
528
        public boolean hasName(String name, Locale locale) {
529
                if (name == null) {
1✔
530
                        return false;
1✔
531
                }
532

533
                if (names == null) {
1✔
NEW
534
                        return false;
×
535
                }
536

537
                for (ConceptName currentName : names) {
1✔
538
                        if (currentName.getVoided() || (locale != null && !currentName.getLocale().equals(locale))) {
1✔
NEW
539
                                continue;
×
540
                        }
541
                        if (name.equalsIgnoreCase(currentName.getName())) {
1✔
542
                                return true;
1✔
543
                        }
544
                }
1✔
545
                
546
                return false;
1✔
547
        }
548
        
549
        /**
550
         * Returns concept name depending of locale, type (short, fully specified, etc) and tag.
551
         * Searches in the locale, and then the locale's parent if nothing is found.
552
         * 
553
         * @param ofType find a name of this type (optional)
554
         * @param havingTag find a name with this tag (optional)
555
         * @param locale find a name with this locale (required)
556
         * @return a name that matches the arguments, or null if none is found. If there are multiple
557
         *         matches and one is locale_preferred, that will be returned, otherwise a random one of
558
         *         the matches will be returned.
559
         * @since 1.9
560
         **/
561
        public ConceptName getName(Locale locale, ConceptNameType ofType, ConceptNameTag havingTag) {
562
                // single pass over the names: keep the first match, but return early on a locale-preferred one
NEW
563
                ConceptName firstMatch = null;
×
NEW
564
                if (names != null) {
×
NEW
565
                        for (ConceptName candidate : names) {
×
NEW
566
                                if (candidate.getVoided() || !candidate.getLocale().equals(locale)) {
×
NEW
567
                                        continue;
×
568
                                }
NEW
569
                                if ((ofType != null && !ofType.equals(candidate.getConceptNameType()))
×
NEW
570
                                        || (havingTag != null && !candidate.hasTag(havingTag))) {
×
NEW
571
                                        continue;
×
572
                                }
NEW
573
                                if (candidate.getLocalePreferred()) {
×
NEW
574
                                        return candidate;
×
575
                                }
NEW
576
                                if (firstMatch == null) {
×
NEW
577
                                        firstMatch = candidate;
×
578
                                }
UNCOV
579
                        }
×
580
                }
NEW
581
                if (firstMatch != null) {
×
NEW
582
                        return firstMatch;
×
583
                }
584

585
                // if we reach here, there were no matching names, so try to look in the parent locale
586
                Locale parent = new Locale(locale.getLanguage());
×
587
                if (!parent.equals(locale)) {
×
588
                        return getName(parent, ofType, havingTag);
×
589
                } else {
590
                        return null;
×
591
                }
592
        }
593
        
594
        /**
595
         * Returns a name in the given locale. If a name isn't found with an exact match, a compatible
596
         * locale match is returned. If no name is found matching either of those, the first name
597
         * defined for this concept is returned.
598
         * 
599
         * @param locale the language and country in which the name is used
600
         * @param exact true/false to return only exact locale (no default locale)
601
         * @return the closest name in the given locale, or the first name
602
         * @see Concept#getNames(Locale) to get all the names for a locale,
603
         * @see Concept#getPreferredName(Locale) for the preferred name (if any)
604
         * <strong>Should</strong> return exact name locale match given exact equals true
605
         * <strong>Should</strong> return loose match given exact equals false
606
         * <strong>Should</strong> return null if no names are found in locale given exact equals true
607
         * <strong>Should</strong> return any name if no locale match given exact equals false
608
         * <strong>Should</strong> return name in broader locale in case none is found in specific one
609
         */
610
        public ConceptName getName(Locale locale, boolean exact) {
611
                
612
                // fail early if this concept has no names defined
613
                if (!hasNonVoidedNames()) {
1✔
614
                        log.debug("there are no names defined for: {}", conceptId);
1✔
615
                        return null;
1✔
616
                }
617
                
618
                log.debug("Getting conceptName for locale: {}", locale);
1✔
619
                
620
                ConceptName exactName = getNameInLocale(locale);
1✔
621
                
622
                if (exactName != null) {
1✔
623
                        return exactName;
1✔
624
                }
625
                
626
                if (!exact) {
1✔
627
                        Locale broaderLocale = new Locale(locale.getLanguage());
1✔
628
                        ConceptName name = getNameInLocale(broaderLocale);
1✔
629
                        return name != null ? name : getName();
1✔
630
                }
631
                return null;
1✔
632
        }
633
        
634
        /**
635
         * Gets the best name in the specified locale.
636
         * 
637
         * @param locale
638
         * @return null if name in given locale doesn't exist
639
         */
640
        private ConceptName getNameInLocale(Locale locale) {
641
                ConceptName preferredName = getPreferredName(locale);
1✔
642
                if (preferredName != null) {
1✔
643
                        return preferredName;
1✔
644
                }
645
                
646
                ConceptName fullySpecifiedName = getFullySpecifiedName(locale);
1✔
647
                if (fullySpecifiedName != null) {
1✔
648
                        return fullySpecifiedName;
×
649
                }
650

651
                // no FSN in this locale: fall back to any synonym in it
652
                if (names != null) {
1✔
653
                        for (ConceptName cn : names) {
1✔
654
                                if (!cn.getVoided() && cn.isSynonym() && cn.getLocale().equals(locale)) {
1✔
655
                                        return cn;
1✔
656
                                }
657
                        }
1✔
658
                }
659
                
660
                return null;
1✔
661
        }
662
        
663
        public ConceptName getPreferredName(Locale forLocale) {
664
                return getPreferredName(forLocale, false);
1✔
665
        }
666
        
667
        /**
668
         * Returns the name which is explicitly marked as preferred for a given locale.
669
         * 
670
         * @param forLocale locale for which to return a preferred name
671
         * @return preferred name for the locale, or null if no preferred name is specified
672
         * <strong>Should</strong> return the concept name explicitly marked as locale preferred
673
         * <strong>Should</strong> return the concept name marked as locale preferred a partial match locale (same language but different country) if no exact match and exact set to false
674
         * <strong>Should</strong> return the fully specified name if no name is explicitly marked as locale preferred and exact set to false
675
         */
676
        public ConceptName getPreferredName(Locale forLocale, Boolean exact) {
677
                
678
                if (log.isDebugEnabled()) {
1✔
679
                        log.debug("Getting preferred conceptName for locale: " + forLocale);
×
680
                }
681
                
682
                if (forLocale == null) {
1✔
683
                        log.warn("Locale cannot be null");
×
684
                        return null;
×
685
                }
686

687
                if (names == null) {
1✔
NEW
688
                        return null;
×
689
                }
690

691
                for (ConceptName nameInLocale : names) {
1✔
692
                        if (!nameInLocale.getVoided() && nameInLocale.getLocale().equals(forLocale)
1✔
693
                                && ObjectUtils.nullSafeEquals(nameInLocale.getLocalePreferred(), true)) {
1✔
694
                                return nameInLocale;
1✔
695
                        }
696
                }
1✔
697
                
698
                if (exact) {
1✔
699
                        return null;
1✔
700
                }
701

702
                // look for partially locale match - any language matches takes precedence over country matches.
703
                String language = forLocale.getLanguage();
1✔
704
                String country = forLocale.getCountry();
1✔
705
                boolean hasCountry = StringUtils.isNotBlank(country);
1✔
706
                ConceptName bestMatch = null;
1✔
707

708
                for (ConceptName nameInLocale : names) {
1✔
709
                        if (nameInLocale.getVoided() || !ObjectUtils.nullSafeEquals(nameInLocale.getLocalePreferred(), true)) {
1✔
710
                                continue;
1✔
711
                        }
712
                        Locale nameLocale = nameInLocale.getLocale();
1✔
713
                        if (language.equals(nameLocale.getLanguage())) {
1✔
714
                                return nameInLocale;
1✔
715
                        }
716
                        if (hasCountry && country.equals(nameLocale.getCountry())) {
1✔
NEW
717
                                bestMatch = nameInLocale;
×
718
                        }
719
                }
1✔
720

721
                if (bestMatch != null) {
1✔
NEW
722
                        return bestMatch;
×
723
                }
724

725
                return getFullySpecifiedName(forLocale);
1✔
726
        }
727
        
728
        /**
729
         * Convenience method that returns the fully specified name in the locale
730
         * 
731
         * @param locale locale from which to look up the fully specified name
732
         * @return the name explicitly marked as fully specified for the locale
733
         * <strong>Should</strong> return the name marked as fully specified for the given locale
734
         */
735
        public ConceptName getFullySpecifiedName(Locale locale) {
736
                if (locale == null || names == null) {
1✔
NEW
737
                        return null;
×
738
                }
739

740
                // the exact-locale pass doubles as the "any names in this locale" guard
741
                boolean anyInLocale = false;
1✔
742
                for (ConceptName conceptName : names) {
1✔
743
                        if (conceptName.getVoided() || !conceptName.getLocale().equals(locale)) {
1✔
744
                                continue;
1✔
745
                        }
746
                        anyInLocale = true;
1✔
747
                        if (ObjectUtils.nullSafeEquals(conceptName.isFullySpecifiedName(), true)) {
1✔
748
                                return conceptName;
1✔
749
                        }
750
                }
1✔
751
                if (!anyInLocale) {
1✔
752
                        return null;
1✔
753
                }
754

755
                // look for partially locale match - any language matches takes precedence over country matches.
756
                String language = locale.getLanguage();
1✔
757
                String country = locale.getCountry();
1✔
758
                boolean hasCountry = StringUtils.isNotBlank(country);
1✔
759
                ConceptName bestMatch = null;
1✔
760

761
                for (ConceptName conceptName : names) {
1✔
762
                        if (conceptName.getVoided() || !ObjectUtils.nullSafeEquals(conceptName.isFullySpecifiedName(), true)) {
1✔
763
                                continue;
1✔
764
                        }
765
                        Locale nameLocale = conceptName.getLocale();
1✔
766
                        if (language.equals(nameLocale.getLanguage())) {
1✔
767
                                return conceptName;
1✔
768
                        }
769
                        if (hasCountry && country.equals(nameLocale.getCountry())) {
1✔
NEW
770
                                bestMatch = conceptName;
×
771
                        }
772
                }
1✔
773
                return bestMatch;
1✔
774
        }
775
        
776
        /**
777
         * Returns all names available in a specific locale. <br>
778
         * <br>
779
         * This is recommended when managing the concept dictionary.
780
         * 
781
         * @param locale locale for which names should be returned
782
         * @return Collection of ConceptNames with the given locale
783
         */
784
        public Collection<ConceptName> getNames(Locale locale) {
785
                return getNames().stream()
1✔
786
                                .filter(n -> n.getLocale().equals(locale))
1✔
787
                                .collect(Collectors.toSet());
1✔
788
        }
789
        
790
        /**
791
         * Returns all names from compatible locales. A locale is considered compatible if it is exactly
792
         * the same locale, or if either locale has no country specified and the language matches. <br>
793
         * <br>
794
         * This is recommended when presenting possible names to the use.
795
         * 
796
         * @param desiredLocale locale with which the names should be compatible
797
         * @return Collection of compatible names
798
         * <strong>Should</strong> exclude incompatible country locales
799
         * <strong>Should</strong> exclude incompatible language locales
800
         */
801
        public List<ConceptName> getCompatibleNames(Locale desiredLocale) {
802
                // lazy create the cache
803
                List<ConceptName> compatibleNames = null;
1✔
804
                if (compatibleCache == null) {
1✔
805
                        compatibleCache = new HashMap<>();
1✔
806
                } else {
807
                        compatibleNames = compatibleCache.get(desiredLocale);
×
808
                }
809
                
810
                if (compatibleNames == null) {
1✔
811
                        compatibleNames = new ArrayList<>();
1✔
812
                        if (names != null) {
1✔
813
                                for (ConceptName possibleName : names) {
1✔
814
                                        if (!possibleName.getVoided() && LocaleUtility.areCompatible(possibleName.getLocale(), desiredLocale)) {
1✔
815
                                                compatibleNames.add(possibleName);
1✔
816
                                        }
817
                                }
1✔
818
                        }
819
                        compatibleCache.put(desiredLocale, compatibleNames);
1✔
820
                }
821
                return compatibleNames;
1✔
822
        }
823
        
824
        /**
825
         * Sets the specified name as the fully specified name for the locale and the current fully
826
         * specified (if any) ceases to be the fully specified name for the locale.
827
         * 
828
         * @param fullySpecifiedName the new fully specified name to set
829
         * <strong>Should</strong> set the concept name type of the specified name to fully specified
830
         * <strong>Should</strong> convert the previous fully specified name if any to a synonym
831
         * <strong>Should</strong> add the name to the list of names if it not among them before
832
         */
833
        public void setFullySpecifiedName(ConceptName fullySpecifiedName) {
834
                if (fullySpecifiedName == null || fullySpecifiedName.getLocale() == null) {
1✔
835
                        throw new APIException(CONCEPT_NAME_LOCALE_NULL, (Object[]) null);
×
836
                } else if (fullySpecifiedName.getVoided()) {
1✔
837
                        throw new APIException("Concept.error.fullySpecifiedName.null", (Object[]) null);
×
838
                }
839
                
840
                ConceptName oldFullySpecifiedName = getFullySpecifiedName(fullySpecifiedName.getLocale());
1✔
841
                if (oldFullySpecifiedName != null) {
1✔
842
                        oldFullySpecifiedName.setConceptNameType(null);
1✔
843
                }
844
                fullySpecifiedName.setConceptNameType(ConceptNameType.FULLY_SPECIFIED);
1✔
845
                //add this name, if it is new or not among this concept's names
846
                if (fullySpecifiedName.getConceptNameId() == null || !containsNonVoided(fullySpecifiedName)) {
1✔
847
                        addName(fullySpecifiedName);
1✔
848
                }
849
        }
1✔
850
        
851
        /**
852
         * Sets the specified name as the short name for the locale and the current shortName(if any)
853
         * ceases to be the short name for the locale.
854
         * 
855
         * @param shortName the new shortName to set
856
         * <strong>Should</strong> set the concept name type of the specified name to short
857
         * <strong>Should</strong> convert the previous shortName if any to a synonym
858
         * <strong>Should</strong> add the name to the list of names if it not among them before
859
         * <strong>Should</strong> void old short name if new one is blank (do not save blanks!)
860
         */
861
        public void setShortName(ConceptName shortName) {
862
                if (shortName != null) {
1✔
863
                        if (shortName.getLocale() == null) {
1✔
864
                                throw new APIException(CONCEPT_NAME_LOCALE_NULL, (Object[]) null);
×
865
                        }
866
                        ConceptName oldShortName = getShortNameInLocale(shortName.getLocale());
1✔
867
                        if (oldShortName != null) {
1✔
868
                                oldShortName.setConceptNameType(null);
1✔
869
                        }
870
                        shortName.setConceptNameType(ConceptNameType.SHORT);
1✔
871
                        if (StringUtils.isNotBlank(shortName.getName())
1✔
872
                                && (shortName.getConceptNameId() == null || !containsNonVoided(shortName))) {
1✔
873
                                //add this name, if it is new or not among this concept's names
874
                                addName(shortName);
1✔
875
                        }
876
                } else {
1✔
877
                        throw new APIException("Concept.error.shortName.null", (Object[]) null);
×
878
                }
879
        }
1✔
880
        
881
        /**
882
         * Gets the explicitly specified short name for a locale.
883
         * 
884
         * @param locale locale for which to find a short name
885
         * @return the short name, or null if none has been explicitly set
886
         */
887
        public ConceptName getShortNameInLocale(Locale locale) {
888
                ConceptName bestMatch = null;
1✔
889
                if (locale != null && names != null) {
1✔
890
                        for (ConceptName shortName : names) {
1✔
891
                                if (shortName.getVoided() || !shortName.isShort()) {
1✔
892
                                        continue;
1✔
893
                                }
894
                                Locale nameLocale = shortName.getLocale();
1✔
895
                                if (nameLocale.equals(locale)) {
1✔
896
                                        return shortName;
1✔
897
                                }
898
                                // test for partially locale match - any language matches takes precedence over country matches.
899
                                if (OpenmrsUtil.nullSafeEquals(locale.getLanguage(), nameLocale.getLanguage())) {
1✔
900
                                        bestMatch = shortName;
1✔
901
                                } else if (bestMatch == null && StringUtils.isNotBlank(locale.getCountry())
1✔
902
                                        && locale.getCountry().equals(nameLocale.getCountry())) {
1✔
903
                                        bestMatch = shortName;
×
904
                                }
905
                        }
1✔
906
                }
907
                return bestMatch;
1✔
908
        }
909
        
910
        /**
911
         * Gets a collection of short names for this concept from all locales.
912
         * 
913
         * @return a collection of all short names for this concept
914
         */
915
        public Collection<ConceptName> getShortNames() {
UNCOV
916
                List<ConceptName> shortNames = new ArrayList<>();
×
UNCOV
917
                if (getNames().isEmpty()) {
×
918
                        if (log.isDebugEnabled()) {
×
919
                                log.debug("The Concept with id: " + conceptId + " has no names");
×
920
                        }
921
                } else {
UNCOV
922
                        shortNames = getNames().stream()
×
UNCOV
923
                                                        .filter(ConceptName::isShort)
×
UNCOV
924
                                                        .collect(Collectors.toList());
×
925
                }
UNCOV
926
                return shortNames;
×
927
        }
928
        
929
        /**
930
         * Returns the short form name for a locale, or if none has been identified, the shortest name
931
         * available in the locale. If exact is false, the shortest name from any locale is returned
932
         * 
933
         * @param locale the language and country in which the short name is used
934
         * @param exact true/false to return only exact locale (no default locale)
935
         * @return the appropriate short name, or null if not found
936
         * <strong>Should</strong> return the name marked as the shortName for the locale if it is present
937
         * <strong>Should</strong> return the shortest name in a given locale for a concept if exact is true
938
         * <strong>Should</strong> return the shortest name for the concept from any locale if exact is false
939
         * <strong>Should</strong> return null if there are no names in the specified locale and exact is true
940
         */
941
        public ConceptName getShortestName(Locale locale, Boolean exact) {
942
                if (log.isDebugEnabled()) {
1✔
943
                        log.debug("Getting shortest conceptName for locale: " + locale);
×
944
                }
945
                
946
                ConceptName shortNameInLocale = getShortNameInLocale(locale);
1✔
947
                if (shortNameInLocale != null) {
1✔
948
                        return shortNameInLocale;
1✔
949
                }
950
                
951
                ConceptName shortestNameForLocale = null;
1✔
952
                ConceptName shortestNameForConcept = null;
1✔
953

954
                if (locale != null && names != null) {
1✔
955
                        for (ConceptName possibleName : names) {
1✔
956
                                if (possibleName.getVoided()) {
1✔
NEW
957
                                        continue;
×
958
                                }
959
                                if (possibleName.getLocale().equals(locale) && ((shortestNameForLocale == null)
1✔
960
                                        || (possibleName.getName().length() < shortestNameForLocale.getName().length()))) {
1✔
961
                                        shortestNameForLocale = possibleName;
1✔
962
                                }
963
                                if ((shortestNameForConcept == null)
1✔
964
                                        || (possibleName.getName().length() < shortestNameForConcept.getName().length())) {
1✔
965
                                        shortestNameForConcept = possibleName;
1✔
966
                                }
967
                        }
1✔
968
                }
969
                
970
                if (exact) {
1✔
971
                        if (shortestNameForLocale == null) {
1✔
972
                                log.warn("No short concept name found for concept id " + conceptId + " for locale "
1✔
973
                                        + locale.getDisplayName());
1✔
974
                        }
975
                        return shortestNameForLocale;
1✔
976
                }
977
                
978
                return shortestNameForConcept;
1✔
979
        }
980
        
981
        /**
982
         * @param name A name
983
         * @return whether this concept has the given name in any locale
984
         */
985
        public boolean isNamed(String name) {
986
                if (names == null) {
1✔
NEW
987
                        return false;
×
988
                }
989
                for (ConceptName cn : names) {
1✔
990
                        if (!cn.getVoided() && name.equals(cn.getName())) {
1✔
991
                                return true;
1✔
992
                        }
NEW
993
                }
×
NEW
994
                return false;
×
995
        }
996
        
997
        /**
998
         * Gets the list of all non-retired concept names which are index terms for this concept
999
         * 
1000
         * @return a collection of concept names which are index terms for this concept
1001
         * @since 1.7
1002
         */
1003
        public Collection<ConceptName> getIndexTerms() {
1004
                return getNames().stream()
×
1005
                                .filter(ConceptName::isIndexTerm)
×
1006
                                .collect(Collectors.toSet());                
×
1007
        }
1008
        
1009
        /**
1010
         * Gets the list of all non-retired concept names which are index terms in a given locale
1011
         * 
1012
         * @param locale the locale for the index terms to return
1013
         * @return a collection of concept names which are index terms in the given locale
1014
         * @since 1.7
1015
         */
1016
        public Collection<ConceptName> getIndexTermsForLocale(Locale locale) {
1017
                return getIndexTerms().stream()
×
1018
                                .filter(n -> n.getLocale().equals(locale))
×
1019
                        .collect(Collectors.toList());
×
1020
        }
1021
        
1022
        /**
1023
         * @return Returns the names.
1024
         */
1025
        public Collection<ConceptName> getNames() {
1026
                return getNames(false);
1✔
1027
        }
1028
        
1029
        /**
1030
         * @return Returns the names.
1031
         * @param includeVoided Include voided ConceptNames if true.
1032
         */
1033
        public Collection<ConceptName> getNames(boolean includeVoided) {
1034
                if (names == null) {
1✔
1035
                        names = new HashSet<>();
×
1036
                }
1037

1038
                return names.stream()
1✔
1039
                                .filter(n -> includeVoided || !n.getVoided())
1✔
1040
                                .collect(Collectors.toSet());
1✔
1041
        }
1042
        
1043
        /**
1044
         * Whether the given name is already among this concept's non-voided names. Scans the underlying
1045
         * collection directly so that no defensive copy is made just to answer a containment question.
1046
         *
1047
         * @param candidate the name to look for
1048
         * @return true if an equal, non-voided name is already attached to this concept
1049
         */
1050
        private boolean containsNonVoided(ConceptName candidate) {
1051
                if (names == null) {
1✔
NEW
1052
                        return false;
×
1053
                }
1054
                for (ConceptName n : names) {
1✔
1055
                        if (!n.getVoided() && n.equals(candidate)) {
1✔
1056
                                return true;
1✔
1057
                        }
1058
                }
1✔
1059
                return false;
1✔
1060
        }
1061

1062
        /**
1063
         * Whether this concept has at least one non-voided name. Scans the underlying collection directly
1064
         * so that the common "no names" guard costs nothing.
1065
         *
1066
         * @return true if any name attached to this concept is not voided
1067
         */
1068
        private boolean hasNonVoidedNames() {
1069
                if (names == null) {
1✔
NEW
1070
                        return false;
×
1071
                }
1072
                for (ConceptName n : names) {
1✔
1073
                        if (!n.getVoided()) {
1✔
1074
                                return true;
1✔
1075
                        }
1076
                }
1✔
1077
                return false;
1✔
1078
        }
1079

1080
        /**
1081
         * @param names The names to set.
1082
         */
1083
        public void setNames(Collection<ConceptName> names) {
1084
                this.names = names;
1✔
1085
        }
1✔
1086
        
1087
        /**
1088
         * Add the given ConceptName to the list of names for this Concept
1089
         * 
1090
         * @param conceptName
1091
         * <strong>Should</strong> replace the old preferred name with a current one
1092
         * <strong>Should</strong> replace the old fully specified name with a current one
1093
         * <strong>Should</strong> replace the old short name with a current one
1094
         * <strong>Should</strong> mark the first name added as fully specified
1095
         */
1096
        public void addName(ConceptName conceptName) {
1097
                if (conceptName != null) {
1✔
1098
                        conceptName.setConcept(this);
1✔
1099
                        if (names == null) {
1✔
1100
                                names = new HashSet<>();
×
1101
                        }
1102
                        if (!names.contains(conceptName)) {
1✔
1103
                                if (getNames().isEmpty()
1✔
1104
                                        && !ConceptNameType.FULLY_SPECIFIED.equals(conceptName.getConceptNameType())) {
1✔
1105
                                        conceptName.setConceptNameType(ConceptNameType.FULLY_SPECIFIED);
1✔
1106
                                } else {
1107
                                        if (conceptName.isPreferred() && !conceptName.isIndexTerm() && conceptName.getLocale() != null) {
1✔
1108
                                                ConceptName prefName = getPreferredName(conceptName.getLocale(), true);
1✔
1109
                                                if (prefName != null) {
1✔
1110
                                                        prefName.setLocalePreferred(false);
1✔
1111
                                                }
1112
                                        }
1113
                                        if (conceptName.isFullySpecifiedName() && conceptName.getLocale() != null) {
1✔
1114
                                                ConceptName fullySpecName = getFullySpecifiedName(conceptName.getLocale());
1✔
1115
                                                if (fullySpecName != null) {
1✔
1116
                                                        fullySpecName.setConceptNameType(null);
1✔
1117
                                                }
1118
                                        } else if (conceptName.isShort() && conceptName.getLocale() != null) {
1✔
1119
                                                ConceptName shortName = getShortNameInLocale(conceptName.getLocale());
1✔
1120
                                                if (shortName != null) {
1✔
1121
                                                        shortName.setConceptNameType(null);
×
1122
                                                }
1123
                                        }
1124
                                }
1125
                                names.add(conceptName);
1✔
1126
                                if (compatibleCache != null) {
1✔
1127
                                        // clear the locale cache, forcing it to be rebuilt
1128
                                        compatibleCache.clear();
×
1129
                                }
1130
                        }
1131
                }
1132
        }
1✔
1133
        
1134
        /**
1135
         * Remove the given name from the list of names for this Concept
1136
         * 
1137
         * @param conceptName
1138
         * @return true if the entity was removed, false otherwise
1139
         */
1140
        public boolean removeName(ConceptName conceptName) {
1141
                if (names != null) {
1✔
1142
                        return names.remove(conceptName);
1✔
1143
                } else {
1144
                        return false;
×
1145
                }
1146
        }
1147
        
1148
        /**
1149
         * Finds the description of the concept using the current locale in Context.getLocale(). Returns
1150
         * null if none found.
1151
         * 
1152
         * @return ConceptDescription attributed to the Concept in the given locale
1153
         */
1154
        public ConceptDescription getDescription() {
1155
                return getDescription(Context.getLocale());
1✔
1156
        }
1157
        
1158
        /**
1159
         * Finds the description of the concept in the given locale. Returns null if none found.
1160
         * 
1161
         * @param locale
1162
         * @return ConceptDescription attributed to the Concept in the given locale
1163
         */
1164
        public ConceptDescription getDescription(Locale locale) {
1165
                return getDescription(locale, false);
1✔
1166
        }
1167
        
1168
        /**
1169
         * Returns the preferred description for a locale.
1170
         * 
1171
         * @param locale the language and country in which the description is used
1172
         * @param exact true/false to return only exact locale (no default locale)
1173
         * @return the appropriate description, or null if not found
1174
         * <strong>Should</strong> return match on locale exactly
1175
         * <strong>Should</strong> return match on language only
1176
         * <strong>Should</strong> not return match on language only if exact match exists
1177
         * <strong>Should</strong> not return language only match for exact matches
1178
         */
1179
        public ConceptDescription getDescription(Locale locale, boolean exact) {
1180
                log.debug("Getting ConceptDescription for locale: " + locale);
1✔
1181
                
1182
                ConceptDescription foundDescription = null;
1✔
1183
                
1184
                if (locale == null) {
1✔
1185
                        locale = LocaleUtility.getDefaultLocale();
×
1186
                }
1187
                
1188
                Locale desiredLocale = locale;
1✔
1189
                
1190
                ConceptDescription defaultDescription = null;
1✔
1191
                for (ConceptDescription availableDescription : getDescriptions()) {
1✔
1192
                        Locale availableLocale = availableDescription.getLocale();
1✔
1193
                        if (availableLocale.equals(desiredLocale)) {
1✔
1194
                                foundDescription = availableDescription;
1✔
1195
                                // skip out now because we found an exact locale match
1196
                                break;
1✔
1197
                        }
1198
                        if (!exact && LocaleUtility.areCompatible(availableLocale, desiredLocale)) {
1✔
1199
                                foundDescription = availableDescription;
1✔
1200
                        }
1201
                        if (availableLocale.equals(LocaleUtility.getDefaultLocale())) {
1✔
1202
                                defaultDescription = availableDescription;
×
1203
                        }
1204
                }
1✔
1205
                
1206
                if (foundDescription == null) {
1✔
1207
                        // no description with the given locale was found.
1208
                        // return null if exact match desired
1209
                        if (exact) {
1✔
1210
                                log.debug("No concept description found for concept id " + conceptId + " for locale "
1✔
1211
                                        + desiredLocale.toString());
1✔
1212
                        } else {
1213
                                // returning default description locale ("en") if exact match
1214
                                // not desired
1215
                                if (defaultDescription == null) {
1✔
1216
                                        log.debug("No concept description found for default locale for concept id " + conceptId);
1✔
1217
                                } else {
1218
                                        foundDescription = defaultDescription;
×
1219
                                }
1220
                        }
1221
                }
1222
                return foundDescription;
1✔
1223
        }
1224
        
1225
        /**
1226
         * @return the retiredBy
1227
         */
1228
        @Override
1229
        public User getRetiredBy() {
1230
                return retiredBy;
1✔
1231
        }
1232
        
1233
        /**
1234
         * @param retiredBy the retiredBy to set
1235
         */
1236
        @Override
1237
        public void setRetiredBy(User retiredBy) {
1238
                this.retiredBy = retiredBy;
1✔
1239
        }
1✔
1240
        
1241
        /**
1242
         * @return the dateRetired
1243
         */
1244
        @Override
1245
        public Date getDateRetired() {
1246
                return dateRetired;
1✔
1247
        }
1248
        
1249
        /**
1250
         * @param dateRetired the dateRetired to set
1251
         */
1252
        @Override
1253
        public void setDateRetired(Date dateRetired) {
1254
                this.dateRetired = dateRetired;
1✔
1255
        }
1✔
1256
        
1257
        /**
1258
         * @return the retireReason
1259
         */
1260
        @Override
1261
        public String getRetireReason() {
1262
                return retireReason;
1✔
1263
        }
1264
        
1265
        /**
1266
         * @param retireReason the retireReason to set
1267
         */
1268
        @Override
1269
        public void setRetireReason(String retireReason) {
1270
                this.retireReason = retireReason;
1✔
1271
        }
1✔
1272
        
1273
        /**
1274
         * @return Returns the descriptions.
1275
         */
1276
        public Collection<ConceptDescription> getDescriptions() {
1277
                if (descriptions == null) {
1✔
1278
                        descriptions = new HashSet<>();
1✔
1279
                }
1280
                return descriptions;
1✔
1281
        }
1282
        
1283
        /**
1284
         * Sets the collection of descriptions for this Concept.
1285
         * 
1286
         * @param descriptions the collection of descriptions
1287
         */
1288
        public void setDescriptions(Collection<ConceptDescription> descriptions) {
1289
                this.descriptions = descriptions;
1✔
1290
        }
1✔
1291
        
1292
        /**
1293
         * Add the given description to the list of descriptions for this Concept
1294
         * 
1295
         * @param description the description to add
1296
         */
1297
        public void addDescription(ConceptDescription description) {
1298
                if (description != null && StringUtils.isNotBlank(description.getDescription()) && !descriptions.contains(description)) {
1✔
1299
                        description.setConcept(this);
1✔
1300
                        descriptions.add(description);
1✔
1301
                }
1302
        }
1✔
1303
        
1304
        /**
1305
         * Remove the given description from the list of descriptions for this Concept
1306
         * 
1307
         * @param description the description to remove
1308
         * @return true if the entity was removed, false otherwise
1309
         * <strong>Should</strong> should remove description passed from list of descriptions
1310
         */
1311
        public boolean removeDescription(ConceptDescription description) {
1312
                return descriptions.remove(description);
1✔
1313
        }
1314
        
1315
        /**
1316
         * @return Returns the retired.
1317
         * 
1318
         * @deprecated as of 2.0, use {@link #getRetired()}
1319
         */
1320
        @Override
1321
        @Deprecated
1322
        @JsonIgnore
1323
        public Boolean isRetired() {
1324
                return getRetired();
1✔
1325
        }
1326
        
1327
        /**
1328
         * This method delegates to {@link #isRetired()}. This is only needed for jstl syntax like
1329
         * ${concept.retired} because the return type is a Boolean object instead of a boolean
1330
         * primitive type.
1331
         * 
1332
         * @see org.openmrs.Retireable#isRetired()
1333
         */
1334
        @Override
1335
        public Boolean getRetired() {
1336
                return retired;
1✔
1337
        }
1338
        
1339
        /**
1340
         * @param retired The retired to set.
1341
         */
1342
        @Override
1343
        public void setRetired(Boolean retired) {
1344
                this.retired = retired;
1✔
1345
        }
1✔
1346
        
1347
        /**
1348
         * Gets the synonyms in the given locale. Returns a list of names from the same language with
1349
         * the preferred synonym sorted first, or an empty list if none found.
1350
         * 
1351
         * @param locale
1352
         * @return Collection of ConceptNames which are synonyms for the Concept in the given locale
1353
         */
1354
        public Collection<ConceptName> getSynonyms(Locale locale) {
1355
                
1356
                List<ConceptName> syns = new ArrayList<>();
1✔
1357
                ConceptName preferredConceptName = null;
1✔
1358
                for (ConceptName possibleSynonymInLoc : getSynonyms()) {
1✔
1359
                        if (locale.equals(possibleSynonymInLoc.getLocale())) {
1✔
1360
                                if (possibleSynonymInLoc.isPreferred()) {
1✔
1361
                                        preferredConceptName = possibleSynonymInLoc;
1✔
1362
                                } else {
1363
                                        syns.add(possibleSynonymInLoc);
1✔
1364
                                }
1365
                        }
1366
                }
1✔
1367
                
1368
                // Add preferred name first in the list.
1369
                if (preferredConceptName != null) {
1✔
1370
                        syns.add(0, preferredConceptName);
1✔
1371
                }
1372
                log.debug("returning: " + syns);
1✔
1373
                return syns;
1✔
1374
        }
1375
        
1376
        /**
1377
         * Gets all the non-retired synonyms.
1378
         * 
1379
         * @return Collection of ConceptNames which are synonyms for the Concept or an empty list if
1380
         *         none is found
1381
         * @since 1.7
1382
         */
1383
        public Collection<ConceptName> getSynonyms() {
1384
                return getNames().stream()
1✔
1385
                                .filter(ConceptName::isSynonym)
1✔
1386
                                .collect(Collectors.toSet());
1✔
1387
        }
1388
        
1389
        /**
1390
         * @return Returns the version.
1391
         */
1392
        public String getVersion() {
1393
                return version;
1✔
1394
        }
1395
        
1396
        /**
1397
         * @param version The version to set.
1398
         */
1399
        public void setVersion(String version) {
1400
                this.version = version;
1✔
1401
        }
1✔
1402
        
1403
        /**
1404
         * @return Returns the conceptSets.
1405
         */
1406
        public Collection<ConceptSet> getConceptSets() {
1407
                return conceptSets;
1✔
1408
        }
1409
        
1410
        /**
1411
         * @param conceptSets The conceptSets to set.
1412
         */
1413
        public void setConceptSets(Collection<ConceptSet> conceptSets) {
1414
                this.conceptSets = conceptSets;
1✔
1415
        }
1✔
1416
        
1417
        /**
1418
         * Whether this concept is numeric or not. This will <i>always</i> return false for concept
1419
         * objects. ConceptNumeric.isNumeric() will then <i>always</i> return true.
1420
         * 
1421
         * @return false
1422
         */
1423
        public boolean isNumeric() {
1424
                return false;
1✔
1425
        }
1426
        
1427
        /**
1428
         * @return the conceptMappings for this concept
1429
         */
1430
        public Collection<ConceptMap> getConceptMappings() {
1431
                if (conceptMappings == null) {
1✔
1432
                        conceptMappings = new HashSet<>();
×
1433
                }
1434
                return conceptMappings;
1✔
1435
        }
1436
        
1437
        /**
1438
         * @param conceptMappings the conceptMappings to set
1439
         */
1440
        public void setConceptMappings(Collection<ConceptMap> conceptMappings) {
1441
                this.conceptMappings = conceptMappings;
1✔
1442
        }
1✔
1443
        
1444
        /**
1445
         * Add the given ConceptMap object to this concept's list of concept mappings. If there is
1446
         * already a corresponding ConceptMap object for this concept already, this one will not be
1447
         * added.
1448
         * 
1449
         * @param newConceptMap
1450
         */
1451
        public void addConceptMapping(ConceptMap newConceptMap) {
1452
                if (newConceptMap != null) {
1✔
1453
                        newConceptMap.setConcept(this);
1✔
1454
                }
1455
                if (newConceptMap != null && !getConceptMappings().contains(newConceptMap)) {
1✔
1456
                        if (newConceptMap.getConceptMapType() == null) {
1✔
1457
                                newConceptMap.setConceptMapType(Context.getConceptService().getDefaultConceptMapType());
1✔
1458
                        }
1459
                        getConceptMappings().add(newConceptMap);
1✔
1460
                }
1461
        }
1✔
1462
        
1463
        /**
1464
         * Child Class ConceptComplex overrides this method and returns true. See
1465
         * {@link org.openmrs.ConceptComplex#isComplex()}. Otherwise this method returns false.
1466
         * 
1467
         * @return false
1468
         * @since 1.5
1469
         */
1470
        public boolean isComplex() {
1471
                return false;
1✔
1472
        }
1473
        
1474
        /**
1475
         * Remove the given ConceptMap from the list of mappings for this Concept
1476
         * 
1477
         * @param conceptMap
1478
         * @return true if the entity was removed, false otherwise
1479
         * <strong>Should</strong> remove concept map passed from list of mappings 
1480
         */
1481
        public boolean removeConceptMapping(ConceptMap conceptMap) {
1482
                return getConceptMappings().remove(conceptMap);
1✔
1483
        }
1484
        
1485
        /**
1486
         * @see java.lang.Object#toString()
1487
         */
1488
        @Override
1489
        public String toString() {
1490
                return "Concept #" + conceptId;
1✔
1491
        }
1492
        
1493
        /**
1494
         * @see org.openmrs.Attributable#findPossibleValues(java.lang.String)
1495
         */
1496
        @Override
1497
        @Deprecated
1498
        public List<Concept> findPossibleValues(String searchText) {
1499
                List<Concept> concepts = new ArrayList<>();
1✔
1500
                try {
1501
                        
1502
                        for (ConceptSearchResult searchResult : Context.getConceptService().getConcepts(searchText,
1✔
1503
                            Collections.singletonList(Context.getLocale()), false, null, null, null, null, null, null, null)) {
1✔
1504
                                concepts.add(searchResult.getConcept());
1✔
1505
                        }
1✔
1506
                }
1507
                catch (Exception e) {
×
1508
                        // pass
1509
                }
1✔
1510
                return concepts;
1✔
1511
        }
1512
        
1513
        /**
1514
         * @see org.openmrs.Attributable#getPossibleValues()
1515
         */
1516
        @Override
1517
        @Deprecated
1518
        public List<Concept> getPossibleValues() {
1519
                try {
1520
                        return Context.getConceptService().getConceptsByName("");
×
1521
                }
1522
                catch (Exception e) {
×
1523
                        // pass
1524
                }
1525
                return Collections.emptyList();
×
1526
        }
1527
        
1528
        /**
1529
         * @see org.openmrs.Attributable#hydrate(java.lang.String)
1530
         */
1531
        @Override
1532
        public Concept hydrate(String reference) {
1533
                try {
1534
                        return Context.getConceptService().getConceptByReference(reference);
1✔
1535
                }
1536
                catch (Exception e) {
×
1537
                        // pass
1538
                }
1539
                return null;
×
1540
        }
1541
        
1542
        /**
1543
         * Turns this concept into a very simple serialized string
1544
         * 
1545
         * @see org.openmrs.Attributable#serialize()
1546
         */
1547
        @Override
1548
        public String serialize() {
1549
                if (this.getConceptId() == null) {
×
1550
                        return "";
×
1551
                }
1552
                
1553
                return "" + this.getConceptId();
×
1554
        }
1555
        
1556
        /**
1557
         * @see org.openmrs.Attributable#getDisplayString()
1558
         */
1559
        @Override
1560
        public String getDisplayString() {
1561
                if (getName() == null) {
1✔
1562
                        return toString();
1✔
1563
                } else {
1564
                        return getName().getName();
1✔
1565
                }
1566
        }
1567
        
1568
        /**
1569
         * Convenience method that returns a set of all the locales in which names have been added for
1570
         * this concept.
1571
         * 
1572
         * @return a set of all locales for names for this concept
1573
         * @since 1.7
1574
         * <strong>Should</strong> return all locales for conceptNames for this concept without duplicates
1575
         */
1576
        public Set<Locale> getAllConceptNameLocales() {
1577
                if (getNames().isEmpty()) {
1✔
1578
                        if (log.isDebugEnabled()) {
×
1579
                                log.debug("The Concept with id: " + conceptId + " has no names");
×
1580
                        }
1581
                        return null;
×
1582
                }
1583
                
1584
                Set<Locale> locales = new HashSet<>();
1✔
1585
                
1586
                for (ConceptName cn : getNames()) {
1✔
1587
                        locales.add(cn.getLocale());
1✔
1588
                }
1✔
1589
                
1590
                return locales;
1✔
1591
        }
1592
        
1593
        /**
1594
         * @since 1.5
1595
         * @see org.openmrs.OpenmrsObject#getId()
1596
         */
1597
        @Override
1598
        public Integer getId() {
1599
                return getConceptId();
1✔
1600
        }
1601
        
1602
        /**
1603
         * @since 1.5
1604
         * @see org.openmrs.OpenmrsObject#setId(java.lang.Integer)
1605
         */
1606
        @Override
1607
        public void setId(Integer id) {
1608
                setConceptId(id);
1✔
1609
        }
1✔
1610
        
1611
        /**
1612
         * Sort the ConceptSet based on the weight
1613
         * 
1614
         * @return sortedConceptSet Collection&lt;ConceptSet&gt;
1615
         */
1616
        private List<ConceptSet> getSortedConceptSets() {
1617
                List<ConceptSet> cs = new ArrayList<>();
1✔
1618
                if (conceptSets != null) {
1✔
1619
                        cs.addAll(conceptSets);
1✔
1620
                        Collections.sort(cs);
1✔
1621
                }
1622
                
1623
                return cs;
1✔
1624
        }
1625
        
1626
        /**
1627
         * Get all the concept members of current concept
1628
         * 
1629
         * @since 1.7
1630
         * @return List&lt;Concept&gt; the Concepts that are members of this Concept's set
1631
         * <strong>Should</strong> return concept set members sorted according to the sort weight
1632
         * <strong>Should</strong> return all the conceptMembers of current Concept
1633
         * <strong>Should</strong> return unmodifiable list of conceptMember list
1634
         * <strong>Should</strong> return concept set members sorted with retired last
1635
         */
1636
        public List<Concept> getSetMembers() {
1637
                List<Concept> conceptMembers = new ArrayList<>();
1✔
1638
                
1639
                Collection<ConceptSet> sortedConceptSet = getSortedConceptSets();
1✔
1640
                
1641
                for (ConceptSet conceptSet : sortedConceptSet) {
1✔
1642
                        conceptMembers.add(conceptSet.getConcept());
1✔
1643
                }
1✔
1644
                return Collections.unmodifiableList(conceptMembers);
1✔
1645
        }
1646

1647
        /**
1648
         * If includeRetired is true, then the returned object is the list of all the concept
1649
         * set members of current concept, else retired concept set members are excluded.
1650
         *
1651
         * @param includeRetired true/false whether to also include/exclude the retired concepts
1652
         * @since 2.5
1653
         */
1654
        public List<Concept> getSetMembers(boolean includeRetired) {
1655
                if (includeRetired) {
1✔
1656
                        return getSetMembers();
1✔
1657
                } else {
1658
                        return getSetMembers().stream()
1✔
1659
                                .filter(a -> !a.getRetired())
1✔
1660
                                .collect(Collectors.toList());
1✔
1661
                }
1662
        }
1663
        
1664
        /**
1665
         * Appends the concept to the end of the existing list of concept members for this Concept
1666
         * 
1667
         * @since 1.7
1668
         * @param setMember Concept to add to the
1669
         * <strong>Should</strong> add concept as a conceptSet
1670
         * <strong>Should</strong> append concept to the existing list of conceptSet
1671
         * <strong>Should</strong> place the new concept last in the list
1672
         * <strong>Should</strong> assign the calling component as parent to the ConceptSet
1673
         */
1674
        public void addSetMember(Concept setMember) {
1675
                addSetMember(setMember, -1);
1✔
1676
        }
1✔
1677
        
1678
        /**
1679
         * Add the concept to the existing member to the list of set members in the given location. <br>
1680
         * <br>
1681
         * index of 0 is before the first concept<br>
1682
         * index of -1 is after last.<br>
1683
         * index of 1 is after the first but before the second, etc<br>
1684
         * 
1685
         * @param setMember the Concept to add as a child of this Concept
1686
         * @param index where in the list of set members to put this setMember
1687
         * @since 1.7
1688
         * <strong>Should</strong> assign the given concept as a ConceptSet
1689
         * <strong>Should</strong> insert the concept before the first with zero index
1690
         * <strong>Should</strong> insert the concept at the end with negative one index
1691
         * <strong>Should</strong> insert the concept in the third slot
1692
         * <strong>Should</strong> assign the calling component as parent to the ConceptSet
1693
         * <strong>Should</strong> add the concept to the current list of conceptSet
1694
         * @see #getSortedConceptSets()
1695
         */
1696
        public void addSetMember(Concept setMember, int index) {
1697
                List<ConceptSet> sortedConceptSets = getSortedConceptSets();
1✔
1698
                int setsSize = sortedConceptSets.size();
1✔
1699
                
1700
                //after sorting, we need to reset the sort weights because retired
1701
                //sets have moved to the bottom and hence need to be reassigned
1702
                //higher sort weights than the non retired ones
1703
                double weight = 990.0;
1✔
1704
                for (ConceptSet conceptSet : sortedConceptSets) {
1✔
1705
                        weight += 10.0;
1✔
1706
                        conceptSet.setSortWeight(weight);
1✔
1707
                }
1✔
1708
                
1709
                if (sortedConceptSets.isEmpty()) {
1✔
1710
                        weight = 1000.0;
1✔
1711
                } else if (index == -1 || index >= setsSize) {
1✔
1712
                        // deals with list size of 1 and any large index given by dev
1713
                        weight = sortedConceptSets.get(setsSize - 1).getSortWeight() + 10.0;
1✔
1714
                } else if (index == 0) {
1✔
1715
                        weight = sortedConceptSets.get(0).getSortWeight() - 10.0;
1✔
1716
                } else {
1717
                        // put the weight between two
1718
                        double prevSortWeight = sortedConceptSets.get(index - 1).getSortWeight();
1✔
1719
                        double nextSortWeight = sortedConceptSets.get(index).getSortWeight();
1✔
1720
                        weight = (prevSortWeight + nextSortWeight) / 2;
1✔
1721
                }
1722
                
1723
                ConceptSet conceptSet = new ConceptSet(setMember, weight);
1✔
1724
                conceptSet.setConceptSet(this);
1✔
1725
                conceptSets.add(conceptSet);
1✔
1726
        }
1✔
1727

1728
        /**
1729
         * @see org.openmrs.customdatatype.Customizable#getAttributes()
1730
         */
1731
        @Override
1732
        public Set<ConceptAttribute> getAttributes() {
1733
                if (attributes == null) {
1✔
1734
                        attributes = new LinkedHashSet<>();
×
1735
                }
1736
                return attributes;
1✔
1737
        }
1738

1739
        /**
1740
         * @see org.openmrs.customdatatype.Customizable#getActiveAttributes()
1741
         */
1742
        @Override
1743
        public Collection<ConceptAttribute> getActiveAttributes() {
1744
                return getAttributes().stream()
1✔
1745
                                .filter(attr -> !attr.getVoided())
1✔
1746
                                .collect(Collectors.toList());
1✔
1747
        }
1748

1749
        /**
1750
         * @see org.openmrs.customdatatype.Customizable#getActiveAttributes(org.openmrs.customdatatype.CustomValueDescriptor)
1751
         */
1752
        @Override
1753
        public List<ConceptAttribute> getActiveAttributes(CustomValueDescriptor ofType) {
1754
                return getAttributes().stream()
×
1755
                                .filter(attr -> attr.getAttributeType().equals(ofType) && !attr.getVoided())
×
1756
                                .collect(Collectors.toList());
×
1757
        }
1758

1759
        /**
1760
         * @param attributes the attributes to set
1761
         */
1762
        public void setAttributes(Set<ConceptAttribute> attributes) {
1763
                this.attributes = attributes;
1✔
1764
        }
1✔
1765

1766
        /**
1767
         * @see org.openmrs.customdatatype.Customizable#addAttribute(Attribute)
1768
         */
1769
        @Override
1770
        public void addAttribute(ConceptAttribute attribute) {
1771
                getAttributes().add(attribute);
×
1772
                attribute.setOwner(this);
×
1773
        }
×
1774

1775
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc