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

openmrs / openmrs-core / 27768587422

18 Jun 2026 02:59PM UTC coverage: 63.728% (+0.05%) from 63.675%
27768587422

push

github

web-flow
Require Get Alerts privilege to read all users' alerts (#6206)

Backport of #6186 to 2.9.x.

AlertService.getAllAlerts() and getAllAlerts(boolean) return every user's
alerts but were guarded only by @Authorized (authentication), so any
authenticated user could read alerts addressed to others. Introduce a
dedicated Get Alerts privilege (GET_* read-privilege convention) and gate both
methods with it. The per-user reads (getAlert, getAlerts, getAlertsByUser,
getAllActiveAlerts) stay open for a caller reading their own alerts; reading
another user's alerts through them now requires Get Alerts. getAlert(Integer)
returns null (rather than throwing) for another user's alert, the same as for
an unknown id, so it cannot be used as an existence oracle.

The privilege is created on startup via @AddOnStartup / checkCoreDataset() and
is not auto-granted to any role, so it does not reintroduce the leak. The
scheduled AlertReminderTask grants itself a proxy Get Alerts privilege around
its read of all alerts.


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

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

10 of 13 new or added lines in 2 files covered. (76.92%)

4 existing lines in 2 files now uncovered.

23940 of 37566 relevant lines covered (63.73%)

0.64 hits per line

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

83.54
/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 || !getNames().contains(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
                ConceptName taggedName = null;
×
420
                for (ConceptName possibleName : getNames()) {
×
421
                        if (possibleName.hasTag(conceptNameTag)) {
×
422
                                taggedName = possibleName;
×
423
                                break;
×
424
                        }
425
                }
×
426
                return taggedName;
×
427
        }
428
        
429
        /**
430
         * Returns a name in the given locale. If a name isn't found with an exact match, a compatible
431
         * locale match is returned. If no name is found matching either of those, the first name
432
         * defined for this concept is returned.
433
         * 
434
         * @param locale the locale to fetch for
435
         * @return ConceptName attributed to the Concept in the given locale
436
         * @since 1.5
437
         * @see Concept#getNames(Locale) to get all the names for a locale,
438
         * @see Concept#getPreferredName(Locale) for the preferred name (if any)
439
         */
440
        public ConceptName getName(Locale locale) {
441
                return getName(locale, false);
1✔
442
        }
443
        
444
        /**
445
         * Returns concept name, the look up for the appropriate name is done in the following order;
446
         * <ul>
447
         * <li>First name found in any locale that is explicitly marked as preferred while searching
448
         * available locales in order of preference (the locales are traversed in their order as they
449
         * are listed in the 'locale.allowed.list' including english global property).</li>
450
         * <li>First "Fully Specified" name found while searching available locales in order of
451
         * preference.</li>
452
         * <li>The first fully specified name found while searching through all names for the concept</li>
453
         * <li>The first synonym found while searching through all names for the concept.</li>
454
         * <li>The first random name found(except index terms) while searching through all names.</li>
455
         * </ul>
456
         * 
457
         * @return {@link ConceptName} in the current locale or any locale if none found
458
         * @since 1.5
459
         * @see Concept#getNames(Locale) to get all the names for a locale
460
         * @see Concept#getPreferredName(Locale) for the preferred name (if any)
461
         * <strong>Should</strong> return the name explicitly marked as locale preferred if any is present
462
         * <strong>Should</strong> return the fully specified name in a locale if no preferred name is set
463
         * <strong>Should</strong> return null if the only added name is an index term
464
         * <strong>Should</strong> return name in broader locale in case none is found in specific one
465
         */
466
        public ConceptName getName() {
467
                if (getNames().isEmpty()) {
1✔
468
                        log.debug("there are no names defined for: {}", conceptId);
1✔
469
                        return null;
1✔
470
                }
471
                
472
                for (Locale currentLocale : LocaleUtility.getLocalesInOrder()) {
1✔
473
                        ConceptName preferredName = getPreferredName(currentLocale);
1✔
474
                        if (preferredName != null) {
1✔
475
                                return preferredName;
1✔
476
                        }
477
                        
478
                        ConceptName fullySpecifiedName = getFullySpecifiedName(currentLocale);
1✔
479
                        if (fullySpecifiedName != null) {
1✔
480
                                return fullySpecifiedName;
×
481
                        }
482
                        
483
                        //if the locale has an variants e.g en_GB, try names in the locale excluding the country code i.e en
484
                        if (!StringUtils.isBlank(currentLocale.getCountry()) || !StringUtils.isBlank(currentLocale.getVariant())) {
1✔
485
                                Locale broaderLocale = new Locale(currentLocale.getLanguage());
1✔
486
                                ConceptName prefNameInBroaderLoc = getPreferredName(broaderLocale);
1✔
487
                                if (prefNameInBroaderLoc != null) {
1✔
488
                                        return prefNameInBroaderLoc;
1✔
489
                                }
490
                                
491
                                ConceptName fullySpecNameInBroaderLoc = getFullySpecifiedName(broaderLocale);
1✔
492
                                if (fullySpecNameInBroaderLoc != null) {
1✔
493
                                        return fullySpecNameInBroaderLoc;
×
494
                                }
495
                        }
496
                }
1✔
497
                
498
                for (ConceptName cn : getNames()) {
1✔
499
                        if (cn.isFullySpecifiedName()) {
1✔
500
                                return cn;
×
501
                        }
502
                }
1✔
503
                
504
                if (!getSynonyms().isEmpty()) {
1✔
505
                        return getSynonyms().iterator().next();
×
506
                }
507
                
508
                // we don't expect to get here since every concept name must have at least
509
                // one fully specified name, but just in case (probably inconsistent data)
510
                
511
                return null;
1✔
512
        }
513
        
514
        /**
515
         * Checks whether this concept has the given string in any of the names in the given locale
516
         * already.
517
         * 
518
         * @param name the ConceptName.name to compare to
519
         * @param locale the locale to look in (null to check all locales)
520
         * @return true/false whether the name exists already
521
         * <strong>Should</strong> return false if name is null
522
         * <strong>Should</strong> return true if locale is null but name exists
523
         * <strong>Should</strong> return false if locale is null but name does not exist
524
         */
525
        public boolean hasName(String name, Locale locale) {
526
                if (name == null) {
1✔
527
                        return false;
1✔
528
                }
529
                
530
                Collection<ConceptName> currentNames;
531
                if (locale == null) {
1✔
532
                        currentNames = getNames();
1✔
533
                } else {
534
                        currentNames = getNames(locale);
1✔
535
                }
536
                
537
                for (ConceptName currentName : currentNames) {
1✔
538
                        if (name.equalsIgnoreCase(currentName.getName())) {
1✔
539
                                return true;
1✔
540
                        }
541
                }
1✔
542
                
543
                return false;
1✔
544
        }
545
        
546
        /**
547
         * Returns concept name depending of locale, type (short, fully specified, etc) and tag.
548
         * Searches in the locale, and then the locale's parent if nothing is found.
549
         * 
550
         * @param ofType find a name of this type (optional)
551
         * @param havingTag find a name with this tag (optional)
552
         * @param locale find a name with this locale (required)
553
         * @return a name that matches the arguments, or null if none is found. If there are multiple
554
         *         matches and one is locale_preferred, that will be returned, otherwise a random one of
555
         *         the matches will be returned.
556
         * @since 1.9
557
         **/
558
        public ConceptName getName(Locale locale, ConceptNameType ofType, ConceptNameTag havingTag) {
559
                Collection<ConceptName> namesInLocale = getNames(locale);
×
560
                if (!namesInLocale.isEmpty()) {
×
561
                        //Pass the possible candidates through a stream and save the ones that match requirements to the list
562
                        List<ConceptName> matches = namesInLocale.stream().filter(
×
563
                                c->(ofType==null || ofType.equals(c.getConceptNameType())) && (havingTag==null || c.hasTag(havingTag))
×
564
                        ).collect(Collectors.toList());
×
565
                        
566
                        // if we have any matches, we'll return one of them
567
                        if (matches.size() == 1) {
×
568
                                return matches.get(0);
×
569
                        } else if (matches.size() > 1) {
×
570
                                for (ConceptName match : matches) {
×
571
                                        if (match.getLocalePreferred()) {
×
572
                                                return match;
×
573
                                        }
574
                                }
×
575
                                // none was explicitly marked as preferred
576
                                return matches.get(0);
×
577
                        }
578
                }
579
                
580
                // if we reach here, there were no matching names, so try to look in the parent locale
581
                Locale parent = new Locale(locale.getLanguage());
×
582
                if (!parent.equals(locale)) {
×
583
                        return getName(parent, ofType, havingTag);
×
584
                } else {
585
                        return null;
×
586
                }
587
        }
588
        
589
        /**
590
         * Returns a name in the given locale. If a name isn't found with an exact match, a compatible
591
         * locale match is returned. If no name is found matching either of those, the first name
592
         * defined for this concept is returned.
593
         * 
594
         * @param locale the language and country in which the name is used
595
         * @param exact true/false to return only exact locale (no default locale)
596
         * @return the closest name in the given locale, or the first name
597
         * @see Concept#getNames(Locale) to get all the names for a locale,
598
         * @see Concept#getPreferredName(Locale) for the preferred name (if any)
599
         * <strong>Should</strong> return exact name locale match given exact equals true
600
         * <strong>Should</strong> return loose match given exact equals false
601
         * <strong>Should</strong> return null if no names are found in locale given exact equals true
602
         * <strong>Should</strong> return any name if no locale match given exact equals false
603
         * <strong>Should</strong> return name in broader locale in case none is found in specific one
604
         */
605
        public ConceptName getName(Locale locale, boolean exact) {
606
                
607
                // fail early if this concept has no names defined
608
                if (getNames().isEmpty()) {
1✔
609
                        log.debug("there are no names defined for: {}", conceptId);
1✔
610
                        return null;
1✔
611
                }
612
                
613
                log.debug("Getting conceptName for locale: {}", locale);
1✔
614
                
615
                ConceptName exactName = getNameInLocale(locale);
1✔
616
                
617
                if (exactName != null) {
1✔
618
                        return exactName;
1✔
619
                }
620
                
621
                if (!exact) {
1✔
622
                        Locale broaderLocale = new Locale(locale.getLanguage());
1✔
623
                        ConceptName name = getNameInLocale(broaderLocale);
1✔
624
                        return name != null ? name : getName();
1✔
625
                }
626
                return null;
1✔
627
        }
628
        
629
        /**
630
         * Gets the best name in the specified locale.
631
         * 
632
         * @param locale
633
         * @return null if name in given locale doesn't exist
634
         */
635
        private ConceptName getNameInLocale(Locale locale) {
636
                ConceptName preferredName = getPreferredName(locale);
1✔
637
                if (preferredName != null) {
1✔
638
                        return preferredName;
1✔
639
                }
640
                
641
                ConceptName fullySpecifiedName = getFullySpecifiedName(locale);
1✔
642
                if (fullySpecifiedName != null) {
1✔
643
                        return fullySpecifiedName;
×
644
                } else if (!getSynonyms(locale).isEmpty()) {
1✔
645
                        return getSynonyms(locale).iterator().next();
1✔
646
                }
647
                
648
                return null;
1✔
649
        }
650
        
651
        public ConceptName getPreferredName(Locale forLocale) {
652
                return getPreferredName(forLocale, false);
1✔
653
        }
654
        
655
        /**
656
         * Returns the name which is explicitly marked as preferred for a given locale.
657
         * 
658
         * @param forLocale locale for which to return a preferred name
659
         * @return preferred name for the locale, or null if no preferred name is specified
660
         * <strong>Should</strong> return the concept name explicitly marked as locale preferred
661
         * <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
662
         * <strong>Should</strong> return the fully specified name if no name is explicitly marked as locale preferred and exact set to false
663
         */
664
        public ConceptName getPreferredName(Locale forLocale, Boolean exact) {
665
                
666
                if (log.isDebugEnabled()) {
1✔
667
                        log.debug("Getting preferred conceptName for locale: " + forLocale);
×
668
                }
669
                
670
                if (forLocale == null) {
1✔
671
                        log.warn("Locale cannot be null");
×
672
                        return null;
×
673
                }
674
                
675
                for (ConceptName nameInLocale : getNames(forLocale)) {
1✔
676
                        if (ObjectUtils.nullSafeEquals(nameInLocale.getLocalePreferred(), true)) {
1✔
677
                                return nameInLocale;
1✔
678
                        }
679
                }
1✔
680
                
681
                if (exact) {
1✔
682
                        return null;
1✔
683
                } else {
684
                        // look for partially locale match - any language matches takes precedence over country matches.
685
                        ConceptName bestMatch = null;
1✔
686

687
                        for (ConceptName nameInLocale : getPartiallyCompatibleNames(forLocale)) {
1✔
688
                                if (ObjectUtils.nullSafeEquals(nameInLocale.getLocalePreferred(), true)) {
1✔
689
                                        Locale nameLocale = nameInLocale.getLocale();
1✔
690
                                        if (forLocale.getLanguage().equals(nameLocale.getLanguage())) {
1✔
691
                                                return nameInLocale;
1✔
692
                                        } else {
693
                                                bestMatch = nameInLocale;
×
694
                                        }
695

696
                                }
697
                        }
1✔
698

699
                        if (bestMatch != null) {
1✔
700
                                return bestMatch;
×
701
                        }
702

703
                        return getFullySpecifiedName(forLocale);
1✔
704
                }
705
        }
706
        
707
        /**
708
         * Convenience method that returns the fully specified name in the locale
709
         * 
710
         * @param locale locale from which to look up the fully specified name
711
         * @return the name explicitly marked as fully specified for the locale
712
         * <strong>Should</strong> return the name marked as fully specified for the given locale
713
         */
714
        public ConceptName getFullySpecifiedName(Locale locale) {
715
                if (locale != null && !getNames(locale).isEmpty()) {
1✔
716
                        //get the first fully specified name, since every concept must have a fully specified name,
717
                        //then, this loop will have to return a name
718
                        for (ConceptName conceptName : getNames(locale)) {
1✔
719
                                if (ObjectUtils.nullSafeEquals(conceptName.isFullySpecifiedName(), true)) {
1✔
720
                                        return conceptName;
1✔
721
                                }
722
                        }
1✔
723
                        
724
                        // look for partially locale match - any language matches takes precedence over country matches.
725
                        ConceptName bestMatch = null;
1✔
726
                        for (ConceptName conceptName : getPartiallyCompatibleNames(locale)) {
1✔
727
                                if (ObjectUtils.nullSafeEquals(conceptName.isFullySpecifiedName(), true)) {
1✔
728
                                        Locale nameLocale = conceptName.getLocale();
1✔
729
                                        if (locale.getLanguage().equals(nameLocale.getLanguage())) {
1✔
730
                                                return conceptName;
1✔
731
                                        }
732
                                        bestMatch = conceptName;
×
733
                                }
734
                        }
1✔
735
                        return bestMatch;
1✔
736
                        
737
                }
738
                return null;
1✔
739
        }
740
        
741
        /**
742
         * Returns all names available in a specific locale. <br>
743
         * <br>
744
         * This is recommended when managing the concept dictionary.
745
         * 
746
         * @param locale locale for which names should be returned
747
         * @return Collection of ConceptNames with the given locale
748
         */
749
        public Collection<ConceptName> getNames(Locale locale) {
750
                return getNames().stream()
1✔
751
                                .filter(n -> n.getLocale().equals(locale))
1✔
752
                                .collect(Collectors.toSet());
1✔
753
        }
754
        
755
        /**
756
         * Returns all names available for locale language "or" country. <br>
757
         * <br>
758
         * 
759
         * @param locale locale for which names should be returned
760
         * @return Collection of ConceptNames with the given locale language or country
761
         */
762
        private Collection<ConceptName> getPartiallyCompatibleNames(Locale locale) {
763
                String language = locale.getLanguage();
1✔
764
                String country = locale.getCountry();
1✔
765
                
766
                return getNames().stream()
1✔
767
                                .filter(n -> language.equals(n.getLocale().getLanguage()) || 
1✔
768
                                                        StringUtils.isNotBlank(country) && country.equals(n.getLocale().getCountry()))
1✔
769
                                .collect(Collectors.toSet());
1✔
770
        }
771
        
772
        /**
773
         * Returns all names from compatible locales. A locale is considered compatible if it is exactly
774
         * the same locale, or if either locale has no country specified and the language matches. <br>
775
         * <br>
776
         * This is recommended when presenting possible names to the use.
777
         * 
778
         * @param desiredLocale locale with which the names should be compatible
779
         * @return Collection of compatible names
780
         * <strong>Should</strong> exclude incompatible country locales
781
         * <strong>Should</strong> exclude incompatible language locales
782
         */
783
        public List<ConceptName> getCompatibleNames(Locale desiredLocale) {
784
                // lazy create the cache
785
                List<ConceptName> compatibleNames = null;
1✔
786
                if (compatibleCache == null) {
1✔
787
                        compatibleCache = new HashMap<>();
1✔
788
                } else {
789
                        compatibleNames = compatibleCache.get(desiredLocale);
×
790
                }
791
                
792
                if (compatibleNames == null) {
1✔
793
                        compatibleNames = new ArrayList<>();
1✔
794
                        for (ConceptName possibleName : getNames()) {
1✔
795
                                if (LocaleUtility.areCompatible(possibleName.getLocale(), desiredLocale)) {
1✔
796
                                        compatibleNames.add(possibleName);
1✔
797
                                }
798
                        }
1✔
799
                        compatibleCache.put(desiredLocale, compatibleNames);
1✔
800
                }
801
                return compatibleNames;
1✔
802
        }
803
        
804
        /**
805
         * Sets the specified name as the fully specified name for the locale and the current fully
806
         * specified (if any) ceases to be the fully specified name for the locale.
807
         * 
808
         * @param fullySpecifiedName the new fully specified name to set
809
         * <strong>Should</strong> set the concept name type of the specified name to fully specified
810
         * <strong>Should</strong> convert the previous fully specified name if any to a synonym
811
         * <strong>Should</strong> add the name to the list of names if it not among them before
812
         */
813
        public void setFullySpecifiedName(ConceptName fullySpecifiedName) {
814
                if (fullySpecifiedName == null || fullySpecifiedName.getLocale() == null) {
1✔
815
                        throw new APIException(CONCEPT_NAME_LOCALE_NULL, (Object[]) null);
×
816
                } else if (fullySpecifiedName.getVoided()) {
1✔
817
                        throw new APIException("Concept.error.fullySpecifiedName.null", (Object[]) null);
×
818
                }
819
                
820
                ConceptName oldFullySpecifiedName = getFullySpecifiedName(fullySpecifiedName.getLocale());
1✔
821
                if (oldFullySpecifiedName != null) {
1✔
822
                        oldFullySpecifiedName.setConceptNameType(null);
1✔
823
                }
824
                fullySpecifiedName.setConceptNameType(ConceptNameType.FULLY_SPECIFIED);
1✔
825
                //add this name, if it is new or not among this concept's names
826
                if (fullySpecifiedName.getConceptNameId() == null || !getNames().contains(fullySpecifiedName)) {
1✔
827
                        addName(fullySpecifiedName);
1✔
828
                }
829
        }
1✔
830
        
831
        /**
832
         * Sets the specified name as the short name for the locale and the current shortName(if any)
833
         * ceases to be the short name for the locale.
834
         * 
835
         * @param shortName the new shortName to set
836
         * <strong>Should</strong> set the concept name type of the specified name to short
837
         * <strong>Should</strong> convert the previous shortName if any to a synonym
838
         * <strong>Should</strong> add the name to the list of names if it not among them before
839
         * <strong>Should</strong> void old short name if new one is blank (do not save blanks!)
840
         */
841
        public void setShortName(ConceptName shortName) {
842
                if (shortName != null) {
1✔
843
                        if (shortName.getLocale() == null) {
1✔
844
                                throw new APIException(CONCEPT_NAME_LOCALE_NULL, (Object[]) null);
×
845
                        }
846
                        ConceptName oldShortName = getShortNameInLocale(shortName.getLocale());
1✔
847
                        if (oldShortName != null) {
1✔
848
                                oldShortName.setConceptNameType(null);
1✔
849
                        }
850
                        shortName.setConceptNameType(ConceptNameType.SHORT);
1✔
851
                        if (StringUtils.isNotBlank(shortName.getName())
1✔
852
                                && (shortName.getConceptNameId() == null || !getNames().contains(shortName))) {
1✔
853
                                //add this name, if it is new or not among this concept's names
854
                                addName(shortName);
1✔
855
                        }
856
                } else {
1✔
857
                        throw new APIException("Concept.error.shortName.null", (Object[]) null);
×
858
                }
859
        }
1✔
860
        
861
        /**
862
         * Gets the explicitly specified short name for a locale.
863
         * 
864
         * @param locale locale for which to find a short name
865
         * @return the short name, or null if none has been explicitly set
866
         */
867
        public ConceptName getShortNameInLocale(Locale locale) {
868
                ConceptName bestMatch = null;
1✔
869
                if (locale != null && !getShortNames().isEmpty()) {
1✔
870
                        for (ConceptName shortName : getShortNames()) {
1✔
871
                                Locale nameLocale = shortName.getLocale();
1✔
872
                                if (nameLocale.equals(locale)) {
1✔
873
                                        return shortName;
1✔
874
                                }
875
                                // test for partially locale match - any language matches takes precedence over country matches.
876
                                if (OpenmrsUtil.nullSafeEquals(locale.getLanguage(), nameLocale.getLanguage())) {
1✔
877
                                        bestMatch = shortName;
1✔
878
                                } else if (bestMatch == null && StringUtils.isNotBlank(locale.getCountry())
1✔
UNCOV
879
                                        && locale.getCountry().equals(nameLocale.getCountry())) {
×
880
                                        bestMatch = shortName;
×
881
                                }
882
                        }
1✔
883
                }
884
                return bestMatch;
1✔
885
        }
886
        
887
        /**
888
         * Gets a collection of short names for this concept from all locales.
889
         * 
890
         * @return a collection of all short names for this concept
891
         */
892
        public Collection<ConceptName> getShortNames() {
893
                List<ConceptName> shortNames = new ArrayList<>();
1✔
894
                if (getNames().isEmpty()) {
1✔
895
                        if (log.isDebugEnabled()) {
×
896
                                log.debug("The Concept with id: " + conceptId + " has no names");
×
897
                        }
898
                } else {
899
                        shortNames = getNames().stream()
1✔
900
                                                        .filter(ConceptName::isShort)
1✔
901
                                                        .collect(Collectors.toList());
1✔
902
                }
903
                return shortNames;
1✔
904
        }
905
        
906
        /**
907
         * Returns the short form name for a locale, or if none has been identified, the shortest name
908
         * available in the locale. If exact is false, the shortest name from any locale is returned
909
         * 
910
         * @param locale the language and country in which the short name is used
911
         * @param exact true/false to return only exact locale (no default locale)
912
         * @return the appropriate short name, or null if not found
913
         * <strong>Should</strong> return the name marked as the shortName for the locale if it is present
914
         * <strong>Should</strong> return the shortest name in a given locale for a concept if exact is true
915
         * <strong>Should</strong> return the shortest name for the concept from any locale if exact is false
916
         * <strong>Should</strong> return null if there are no names in the specified locale and exact is true
917
         */
918
        public ConceptName getShortestName(Locale locale, Boolean exact) {
919
                if (log.isDebugEnabled()) {
1✔
920
                        log.debug("Getting shortest conceptName for locale: " + locale);
×
921
                }
922
                
923
                ConceptName shortNameInLocale = getShortNameInLocale(locale);
1✔
924
                if (shortNameInLocale != null) {
1✔
925
                        return shortNameInLocale;
1✔
926
                }
927
                
928
                ConceptName shortestNameForLocale = null;
1✔
929
                ConceptName shortestNameForConcept = null;
1✔
930
                
931
                if (locale != null) {
1✔
932
                        for (ConceptName possibleName : getNames()) {
1✔
933
                                if (possibleName.getLocale().equals(locale)
1✔
934
                                        && ((shortestNameForLocale == null) || (possibleName.getName().length() < shortestNameForLocale
1✔
935
                                                .getName().length()))) {
1✔
936
                                        shortestNameForLocale = possibleName;
1✔
937
                                }
938
                                if ((shortestNameForConcept == null)
1✔
939
                                        || (possibleName.getName().length() < shortestNameForConcept.getName().length())) {
1✔
940
                                        shortestNameForConcept = possibleName;
1✔
941
                                }
942
                        }
1✔
943
                }
944
                
945
                if (exact) {
1✔
946
                        if (shortestNameForLocale == null) {
1✔
947
                                log.warn("No short concept name found for concept id " + conceptId + " for locale "
1✔
948
                                        + locale.getDisplayName());
1✔
949
                        }
950
                        return shortestNameForLocale;
1✔
951
                }
952
                
953
                return shortestNameForConcept;
1✔
954
        }
955
        
956
        /**
957
         * @param name A name
958
         * @return whether this concept has the given name in any locale
959
         */
960
        public boolean isNamed(String name) {
961
                return getNames().stream().anyMatch(cn -> name.equals(cn.getName()));
1✔
962
        }
963
        
964
        /**
965
         * Gets the list of all non-retired concept names which are index terms for this concept
966
         * 
967
         * @return a collection of concept names which are index terms for this concept
968
         * @since 1.7
969
         */
970
        public Collection<ConceptName> getIndexTerms() {
971
                return getNames().stream()
×
972
                                .filter(ConceptName::isIndexTerm)
×
973
                                .collect(Collectors.toSet());                
×
974
        }
975
        
976
        /**
977
         * Gets the list of all non-retired concept names which are index terms in a given locale
978
         * 
979
         * @param locale the locale for the index terms to return
980
         * @return a collection of concept names which are index terms in the given locale
981
         * @since 1.7
982
         */
983
        public Collection<ConceptName> getIndexTermsForLocale(Locale locale) {
984
                return getIndexTerms().stream()
×
985
                                .filter(n -> n.getLocale().equals(locale))
×
986
                        .collect(Collectors.toList());
×
987
        }
988
        
989
        /**
990
         * @return Returns the names.
991
         */
992
        public Collection<ConceptName> getNames() {
993
                return getNames(false);
1✔
994
        }
995
        
996
        /**
997
         * @return Returns the names.
998
         * @param includeVoided Include voided ConceptNames if true.
999
         */
1000
        public Collection<ConceptName> getNames(boolean includeVoided) {
1001
                if (names == null) {
1✔
1002
                        names = new HashSet<>();
×
1003
                }
1004

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

1577
        /**
1578
         * If includeRetired is true, then the returned object is the list of all the concept
1579
         * set members of current concept, else retired concept set members are excluded.
1580
         *
1581
         * @param includeRetired true/false whether to also include/exclude the retired concepts
1582
         * @since 2.5
1583
         */
1584
        public List<Concept> getSetMembers(boolean includeRetired) {
1585
                if (includeRetired) {
1✔
1586
                        return getSetMembers();
1✔
1587
                } else {
1588
                        return getSetMembers().stream()
1✔
1589
                                .filter(a -> !a.getRetired())
1✔
1590
                                .collect(Collectors.toList());
1✔
1591
                }
1592
        }
1593
        
1594
        /**
1595
         * Appends the concept to the end of the existing list of concept members for this Concept
1596
         * 
1597
         * @since 1.7
1598
         * @param setMember Concept to add to the
1599
         * <strong>Should</strong> add concept as a conceptSet
1600
         * <strong>Should</strong> append concept to the existing list of conceptSet
1601
         * <strong>Should</strong> place the new concept last in the list
1602
         * <strong>Should</strong> assign the calling component as parent to the ConceptSet
1603
         */
1604
        public void addSetMember(Concept setMember) {
1605
                addSetMember(setMember, -1);
1✔
1606
        }
1✔
1607
        
1608
        /**
1609
         * Add the concept to the existing member to the list of set members in the given location. <br>
1610
         * <br>
1611
         * index of 0 is before the first concept<br>
1612
         * index of -1 is after last.<br>
1613
         * index of 1 is after the first but before the second, etc<br>
1614
         * 
1615
         * @param setMember the Concept to add as a child of this Concept
1616
         * @param index where in the list of set members to put this setMember
1617
         * @since 1.7
1618
         * <strong>Should</strong> assign the given concept as a ConceptSet
1619
         * <strong>Should</strong> insert the concept before the first with zero index
1620
         * <strong>Should</strong> insert the concept at the end with negative one index
1621
         * <strong>Should</strong> insert the concept in the third slot
1622
         * <strong>Should</strong> assign the calling component as parent to the ConceptSet
1623
         * <strong>Should</strong> add the concept to the current list of conceptSet
1624
         * @see #getSortedConceptSets()
1625
         */
1626
        public void addSetMember(Concept setMember, int index) {
1627
                List<ConceptSet> sortedConceptSets = getSortedConceptSets();
1✔
1628
                int setsSize = sortedConceptSets.size();
1✔
1629
                
1630
                //after sorting, we need to reset the sort weights because retired
1631
                //sets have moved to the bottom and hence need to be reassigned
1632
                //higher sort weights than the non retired ones
1633
                double weight = 990.0;
1✔
1634
                for (ConceptSet conceptSet : sortedConceptSets) {
1✔
1635
                        weight += 10.0;
1✔
1636
                        conceptSet.setSortWeight(weight);
1✔
1637
                }
1✔
1638
                
1639
                if (sortedConceptSets.isEmpty()) {
1✔
1640
                        weight = 1000.0;
1✔
1641
                } else if (index == -1 || index >= setsSize) {
1✔
1642
                        // deals with list size of 1 and any large index given by dev
1643
                        weight = sortedConceptSets.get(setsSize - 1).getSortWeight() + 10.0;
1✔
1644
                } else if (index == 0) {
1✔
1645
                        weight = sortedConceptSets.get(0).getSortWeight() - 10.0;
1✔
1646
                } else {
1647
                        // put the weight between two
1648
                        double prevSortWeight = sortedConceptSets.get(index - 1).getSortWeight();
1✔
1649
                        double nextSortWeight = sortedConceptSets.get(index).getSortWeight();
1✔
1650
                        weight = (prevSortWeight + nextSortWeight) / 2;
1✔
1651
                }
1652
                
1653
                ConceptSet conceptSet = new ConceptSet(setMember, weight);
1✔
1654
                conceptSet.setConceptSet(this);
1✔
1655
                conceptSets.add(conceptSet);
1✔
1656
        }
1✔
1657

1658
        /**
1659
         * @see org.openmrs.customdatatype.Customizable#getAttributes()
1660
         */
1661
        @Override
1662
        public Set<ConceptAttribute> getAttributes() {
1663
                if (attributes == null) {
1✔
1664
                        attributes = new LinkedHashSet<>();
×
1665
                }
1666
                return attributes;
1✔
1667
        }
1668

1669
        /**
1670
         * @see org.openmrs.customdatatype.Customizable#getActiveAttributes()
1671
         */
1672
        @Override
1673
        public Collection<ConceptAttribute> getActiveAttributes() {
1674
                return getAttributes().stream()
1✔
1675
                                .filter(attr -> !attr.getVoided())
1✔
1676
                                .collect(Collectors.toList());
1✔
1677
        }
1678

1679
        /**
1680
         * @see org.openmrs.customdatatype.Customizable#getActiveAttributes(org.openmrs.customdatatype.CustomValueDescriptor)
1681
         */
1682
        @Override
1683
        public List<ConceptAttribute> getActiveAttributes(CustomValueDescriptor ofType) {
1684
                return getAttributes().stream()
×
1685
                                .filter(attr -> attr.getAttributeType().equals(ofType) && !attr.getVoided())
×
1686
                                .collect(Collectors.toList());
×
1687
        }
1688

1689
        /**
1690
         * @param attributes the attributes to set
1691
         */
1692
        public void setAttributes(Set<ConceptAttribute> attributes) {
1693
                this.attributes = attributes;
1✔
1694
        }
1✔
1695

1696
        /**
1697
         * @see org.openmrs.customdatatype.Customizable#addAttribute(Attribute)
1698
         */
1699
        @Override
1700
        public void addAttribute(ConceptAttribute attribute) {
1701
                getAttributes().add(attribute);
×
1702
                attribute.setOwner(this);
×
1703
        }
×
1704

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

© 2026 Coveralls, Inc