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

openmrs / openmrs-core / 36456271610

28 Sep 2026 04:53PM UTC coverage: 66.105% (+1.2%) from 64.906%
36456271610

push

github

ibacher
Clear leaked outbox rows before OutboxEventServiceTest tests (#6596)

Global property saves committed by earlier tests in the shared context
are persisted as outbox events via TestTransactionalEventAggregator.
When more than 100 are pending, getProcessingAndPendingEvents (ordered
by id, limit 100) no longer returns the test's own events, so the test
failed whenever the module tests ran before it, as on Linux CI.


Claude-Session: https://claude.ai/code/session_015sv8E9mrTQK5LBCaS5zqCn

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

25547 of 38646 relevant lines covered (66.11%)

0.66 hits per line

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

81.75
/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) {
419
                if (names == null) {
×
420
                        return null;
×
421
                }
422
                for (ConceptName possibleName : names) {
×
423
                        if (!possibleName.getVoided() && possibleName.hasTag(conceptNameTag)) {
×
424
                                return possibleName;
×
425
                        }
426
                }
×
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 ✔
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 ✔
534
                        return false;
×
535
                }
536

537
                for (ConceptName currentName : names) {
1 ✔
538
                        if (currentName.getVoided() || (locale != null && !currentName.getLocale().equals(locale))) {
1 ✔
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
563
                ConceptName firstMatch = null;
×
564
                if (names != null) {
×
565
                        for (ConceptName candidate : names) {
×
566
                                if (candidate.getVoided() || !candidate.getLocale().equals(locale)) {
×
567
                                        continue;
×
568
                                }
569
                                if ((ofType != null && !ofType.equals(candidate.getConceptNameType()))
×
570
                                        || (havingTag != null && !candidate.hasTag(havingTag))) {
×
571
                                        continue;
×
572
                                }
573
                                if (candidate.getLocalePreferred()) {
×
574
                                        return candidate;
×
575
                                }
576
                                if (firstMatch == null) {
×
577
                                        firstMatch = candidate;
×
578
                                }
579
                        }
×
580
                }
581
                if (firstMatch != null) {
×
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 ✔
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 ✔
717
                                bestMatch = nameInLocale;
×
718
                        }
719
                }
1 ✔
720

721
                if (bestMatch != null) {
1 ✔
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 ✔
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 ✔
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())) {
×
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() {
916
                List<ConceptName> shortNames = new ArrayList<>();
×
917
                if (getNames().isEmpty()) {
×
918
                        if (log.isDebugEnabled()) {
×
919
                                log.debug("The Concept with id: " + conceptId + " has no names");
×
920
                        }
921
                } else {
922
                        shortNames = getNames().stream()
×
923
                                                        .filter(ConceptName::isShort)
×
924
                                                        .collect(Collectors.toList());
×
925
                }
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 ✔
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 ✔
987
                        return false;
×
988
                }
989
                for (ConceptName cn : names) {
1 ✔
990
                        if (!cn.getVoided() && name.equals(cn.getName())) {
1 ✔
991
                                return true;
1 ✔
992
                        }
993
                }
×
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 ✔
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 ✔
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