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

openmrs / openmrs-core / 22629535597

03 Mar 2026 03:16PM UTC coverage: 65.387% (-0.007%) from 65.394%
22629535597

push

github

ibacher
TRUNK-6512: Introduce ConceptReferenceRangeContext API (#5880)

Co-authored-by: Binayak490-cyber <binayak490@gmail.com>

104 of 129 new or added lines in 4 files covered. (80.62%)

4 existing lines in 4 files now uncovered.

23838 of 36457 relevant lines covered (65.39%)

0.65 hits per line

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

81.1
/api/src/main/java/org/openmrs/util/ConceptReferenceRangeUtility.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.util;
11

12
import java.io.StringWriter;
13
import java.time.LocalDate;
14
import java.time.ZoneId;
15
import java.time.temporal.ChronoUnit;
16
import java.util.ArrayList;
17
import java.util.Collections;
18
import java.util.Date;
19
import java.util.List;
20
import java.util.Locale;
21
import java.util.Properties;
22

23
import org.apache.commons.lang.StringUtils;
24
import org.apache.velocity.VelocityContext;
25
import org.apache.velocity.app.VelocityEngine;
26
import org.apache.velocity.exception.ParseErrorException;
27
import org.apache.velocity.runtime.log.Log4JLogChute;
28
import org.joda.time.LocalTime;
29
import org.openmrs.Concept;
30
import org.openmrs.ConceptReferenceRangeContext;
31
import org.openmrs.Obs;
32
import org.openmrs.Patient;
33
import org.openmrs.PatientProgram;
34
import org.openmrs.PatientState;
35
import org.openmrs.Person;
36
import org.openmrs.api.APIException;
37
import org.openmrs.api.context.Context;
38
import org.openmrs.api.db.hibernate.HibernateUtil;
39

40
/**
41
 * A utility class that evaluates the concept ranges 
42
 * 
43
 * @since 2.7.0
44
 */
45
public class ConceptReferenceRangeUtility {
46
        
47
        private final long NULL_DATE_RETURN_VALUE = -1;
1✔
48
        
49
        public ConceptReferenceRangeUtility() {
1✔
50
        }
1✔
51
        
52
        /**
53
         * This method evaluates the given criteria against the provided {@link Obs}.
54
         *
55
         * @param criteria the criteria string to evaluate e.g. "$patient.getAge() > 1"
56
         * @param obs The observation (Obs) object containing the values to be used in the criteria evaluation.
57
         *                  
58
         * @return true if the criteria evaluates to true, false otherwise
59
         */
60
        public boolean evaluateCriteria(String criteria, Obs obs) {
61
                if (obs == null) {
1✔
62
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: Obs is null");
1✔
63
                }
64

65
                if (obs.getPerson() == null) {
1✔
66
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: patient is null");
×
67
                }
68

69
                if (StringUtils.isBlank(criteria)) {
1✔
70
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: criteria is empty");
1✔
71
                }
72

73
                return evaluateCriteria(criteria, new ConceptReferenceRangeContext(obs));
1✔
74
        }
75

76
        /**
77
         * Evaluates criteria against a {@link ConceptReferenceRangeContext}. When the context was
78
         * constructed from an Obs, {@code $obs} is available in the expression; otherwise only
79
         * {@code $patient}, {@code $fn}, {@code $context}, {@code $date}, and {@code $encounter}
80
         * are available.
81
         *
82
         * @param criteria the criteria string to evaluate
83
         * @param context the evaluation context
84
         * @return true if the criteria evaluates to true, false otherwise
85
         *
86
         * @since 3.0.0, 2.9.0, 2.8.5, 2.7.9
87
         */
88
        public boolean evaluateCriteria(String criteria, ConceptReferenceRangeContext context) {
89
                if (context == null) {
1✔
NEW
90
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: context is null");
×
91
                }
92

93
                if (context.getPerson() == null) {
1✔
NEW
94
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: patient is null");
×
95
                }
96

97
                if (StringUtils.isBlank(criteria)) {
1✔
NEW
98
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: criteria is empty");
×
99
                }
100

101
                VelocityContext velocityContext = new VelocityContext();
1✔
102
                velocityContext.put("fn", this);
1✔
103
                velocityContext.put("patient", HibernateUtil.getRealObjectFromProxy(context.getPerson()));
1✔
104
                velocityContext.put("context", context);
1✔
105

106
                velocityContext.put("obs", context.getObs());
1✔
107
                velocityContext.put("encounter", context.getEncounter());
1✔
108
                velocityContext.put("date", context.getDate());
1✔
109

110
                VelocityEngine velocityEngine = new VelocityEngine();
1✔
111
                try {
112
                        Properties props = new Properties();
1✔
113
                        props.put("runtime.log.logsystem.class", Log4JLogChute.class.getName());
1✔
114
                        props.put("runtime.log.logsystem.log4j.category", "velocity");
1✔
115
                        props.put("runtime.log.logsystem.log4j.logger", "velocity");
1✔
116
                        velocityEngine.init(props);
1✔
117
                }
118
                catch (Exception e) {
×
119
                        throw new APIException("Failed to create the velocity engine: " + e.getMessage(), e);
×
120
                }
1✔
121

122
                StringWriter writer = new StringWriter();
1✔
123
                String wrappedCriteria = "#set( $criteria = " + criteria + " )$criteria";
1✔
124

125
                try {
126
                        velocityEngine.evaluate(velocityContext, writer, ConceptReferenceRangeUtility.class.getName(), wrappedCriteria);
1✔
127
                        return Boolean.parseBoolean(writer.toString());
1✔
128
                }
129
                catch (ParseErrorException e) {
1✔
130
                        throw new APIException("An error occurred while evaluating criteria. Invalid criteria: " + criteria, e);
1✔
131
                }
132
                catch (Exception e) {
×
133
                        throw new APIException("An error occurred while evaluating criteria: ", e);
×
134
                }
135
        }
136

137
        /**
138
         * Gets the latest Obs by concept.
139
         *
140
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
141
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
142
         * @param person person to get obs for
143
         *                   
144
         * @return Obs latest Obs
145
         */
146
        public Obs getLatestObs(String conceptRef, Person person) {
147
                if (person == null) {
1✔
NEW
148
                        return null;
×
149
                }
150
                Concept concept = Context.getConceptService().getConceptByReference(conceptRef);
1✔
151

152
                if (concept != null) {
1✔
153
                        List<Obs> observations = Context.getObsService().getObservations(
1✔
154
                                Collections.singletonList(person), 
1✔
155
                                null, 
156
                                Collections.singletonList(concept), 
1✔
157
                                null, 
158
                                null, 
159
                                null,
160
                                Collections.singletonList("dateCreated"), 
1✔
161
                                1, 
1✔
162
                                null,
163
                                null, 
164
                                null, 
165
                                false
166
                        );
167

168
                        return observations.isEmpty() ? null : observations.get(0);
1✔
169
                }
170

171
                return null;
1✔
172
        }
173
        
174
        /**
175
         * Gets the time of the day in hours.
176
         *
177
         * @return the hour of the day in 24hr format (e.g. 14 to mean 2pm)
178
         */
179
        public int getCurrentHour() {
180
                return LocalTime.now().getHourOfDay();
1✔
181
        }
182
        
183
        /**
184
         * Retrieves the most relevant Obs for the given current Obs and conceptRef. If the current Obs contains a valid value 
185
         * (coded, numeric, date, text e.t.c) and the concept in Obs is the same as the supplied concept,
186
         * the method returns the current Obs. Otherwise, it fetches the latest Obs for the supplied concept and patient.
187
         *
188
         * @param currentObs the current Obs being evaluated
189
         * @return the most relevant Obs based on the current Obs, or the latest Obs if the current one has no valid value
190
         */
191
        public Obs getCurrentObs(String conceptRef, Obs currentObs) {
192
                Concept concept = Context.getConceptService().getConceptByReference(conceptRef);
1✔
193
                
194
                if (currentObs.getValueAsString(Locale.ENGLISH).isEmpty() && (concept != null && concept == currentObs.getConcept())) {
1✔
195
                        return currentObs;
×
196
                } else {
197
                        return getLatestObs(conceptRef, currentObs.getPerson());
1✔
198
                }
199
        }
200
        
201
        /**
202
         * Gets the person's latest observation date for a given concept
203
         * 
204
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
205
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
206
         * @param person the person
207
         * 
208
         * @return the observation date
209
         * 
210
         * @since 2.7.8
211
         */
212
        public Date getLatestObsDate(String conceptRef, Person person) {
213
                Obs obs = getLatestObs(conceptRef, person);
1✔
214
                if (obs == null) {
1✔
215
                        return null;
×
216
                }
217
                
218
                Date date = obs.getValueDate();
1✔
219
                if (date == null) {
1✔
220
                        date = obs.getValueDatetime();
1✔
221
                }
222
                
223
                return date;
1✔
224
        }
225
        
226
        /**
227
         * Checks if an observation's value coded answer is equal to a given concept
228
         * 
229
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
230
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434" for the observation's question
231
         *                   
232
         * @param person the person
233
         * 
234
         * @param answerConceptRef can be either concept uuid or conceptMap's code and sourceName 
235
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434" for the observation's coded answer
236
         *                   
237
         * @return true if the given concept is equal to the observation's value coded answer
238
         * 
239
         * @since 2.7.8
240
         */
241
        public boolean isObsValueCodedAnswer(String conceptRef, Person person, String answerConceptRef) {
242
                Obs obs = getLatestObs(conceptRef, person);
1✔
243
                if (obs == null) {
1✔
244
                        return false;
×
245
                }
246
                
247
                Concept valudeCoded = obs.getValueCoded();
1✔
248
                if (valudeCoded == null) {
1✔
249
                        return false;
×
250
                }
251
                
252
                Concept answerConcept = Context.getConceptService().getConceptByReference(answerConceptRef);
1✔
253
                if (answerConcept == null) {
1✔
254
                        return false;
×
255
                }
256
                
257
                return valudeCoded.equals(answerConcept);
1✔
258
        }
259
        
260
        /**
261
         * Gets the number of days from the person's latest observation date value for a given concept to the current date
262
         * 
263
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
264
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
265
         * @param person the person
266
         * 
267
         * @return the number of days
268
         * 
269
         * @since 2.7.8
270
         */
271
        public long getObsDays(String conceptRef, Person person) {
272
                Date date = getLatestObsDate(conceptRef, person);
1✔
273
                if (date == null) {
1✔
274
                        return NULL_DATE_RETURN_VALUE;
×
275
                }
276
                return this.getDays(date);
1✔
277
        }
278
        
279
        /**
280
         * Gets the number of weeks from the person's latest observation date value for a given concept to the current date
281
         * 
282
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
283
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
284
         * @param person the person
285
         * 
286
         * @return the number of weeks
287
         * 
288
         * @since 2.7.8
289
         */
290
        public long getObsWeeks(String conceptRef, Person person) {
291
                Date date = getLatestObsDate(conceptRef, person);
1✔
292
                if (date == null) {
1✔
293
                        return NULL_DATE_RETURN_VALUE;
1✔
294
                }
295
                return this.getWeeks(date);
1✔
296
        }
297
        
298
        /**
299
         * Gets the number of months from the person's latest observation date value for a given concept to the current date
300
         * 
301
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
302
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
303
         * @param person the person
304
         * 
305
         * @return the number of months
306
         * 
307
         * @since 2.7.8
308
         */
309
        public long getObsMonths(String conceptRef, Person person) {
310
                Date date = getLatestObsDate(conceptRef, person);
1✔
311
                if (date == null) {
1✔
312
                        return NULL_DATE_RETURN_VALUE;
×
313
                }
314
                return this.getMonths(date);
1✔
315
        }
316
        
317
        /**
318
         * Gets the number of years from the person's latest observation date value for a given concept to the current date
319
         * 
320
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
321
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
322
         * @param person the person
323
         * 
324
         * @return the number of years
325
         * 
326
         * @since 2.7.8
327
         */
328
        public long getObsYears(String conceptRef, Person person) {
329
                Date date = getLatestObsDate(conceptRef, person);
1✔
330
                if (date == null) {
1✔
331
                        return NULL_DATE_RETURN_VALUE;
×
332
                }
333
                return this.getYears(date);
1✔
334
        }
335
        
336
        /**
337
         * Gets the number of days between two given dates
338
         * 
339
         * @param fromDate the date from which to start counting
340
         * @param toDate the date up to which to stop counting
341
         * 
342
         * @return the number of days between
343
         * 
344
         * @since 2.7.8
345
         */
346
        public long getDaysBetween(Date fromDate, Date toDate) {
347
                if (fromDate == null || toDate == null) {
1✔
348
                        return NULL_DATE_RETURN_VALUE;
×
349
                }
350
                return ChronoUnit.DAYS.between(toLocalDate(fromDate), toLocalDate(toDate));
1✔
351
        }
352
        
353
        /**
354
         * Gets the number of weeks between two given dates
355
         * 
356
         * @param fromDate the date from which to start counting
357
         * @param toDate the date up to which to stop counting
358
         * 
359
         * @return the number of weeks between
360
         * 
361
         * @since 2.7.8
362
         */
363
        public long getWeeksBetween(Date fromDate, Date toDate) {
364
                if (fromDate == null || toDate == null) {
1✔
365
                        return NULL_DATE_RETURN_VALUE;
×
366
                }
367
                return ChronoUnit.WEEKS.between(toLocalDate(fromDate), toLocalDate(toDate));
1✔
368
        }
369
        
370
        /**
371
         * Gets the number of months between two given dates
372
         * 
373
         * @param fromDate the date from which to start counting
374
         * @param toDate the date up to which to stop counting
375
         * 
376
         * @return the number of months between
377
         * 
378
         * @since 2.7.8
379
         */
380
        public long getMonthsBetween(Date fromDate, Date toDate) {
381
                if (fromDate == null || toDate == null) {
1✔
382
                        return NULL_DATE_RETURN_VALUE;
×
383
                }
384
                return ChronoUnit.MONTHS.between(toLocalDate(fromDate), toLocalDate(toDate));
1✔
385
        }
386
        
387
        /**
388
         * Gets the number of years between two given dates
389
         * 
390
         * @param fromDate the date from which to start counting
391
         * @param toDate the date up to which to stop counting
392
         * 
393
         * @return the number of years between
394
         * 
395
         * @since 2.7.8
396
         */
397
        public long getYearsBetween(Date fromDate, Date toDate) {
398
                if (fromDate == null || toDate == null) {
1✔
399
                        return NULL_DATE_RETURN_VALUE;
×
400
                }
401
                return ChronoUnit.YEARS.between(toLocalDate(fromDate), toLocalDate(toDate));
1✔
402
        }
403
        
404
        /**
405
         * Gets the number of days from a given date up to the current date.
406
         * 
407
         * @param fromDate the date from which to start counting
408
         * @return the number of days
409
         * 
410
         * @since 2.7.8
411
         */
412
        public long getDays(Date fromDate) {
413
                return getDaysBetween(fromDate, new Date());
1✔
414
        }
415
        
416
        /**
417
         * Gets the number of weeks from a given date up to the current date.
418
         * 
419
         * @param fromDate the date from which to start counting
420
         * @return the number of weeks
421
         * 
422
         * @since 2.7.8
423
         */
424
        public long getWeeks(Date fromDate) {
425
                return getWeeksBetween(fromDate, new Date());
1✔
426
        }
427
        
428
        /**
429
         * Gets the number of months from a given date up to the current date.
430
         * 
431
         * @param fromDate the date from which to start counting
432
         * @return the number of months
433
         * 
434
         * @since 2.7.8
435
         */
436
        public long getMonths(Date fromDate) {
437
                return getMonthsBetween(fromDate, new Date());
1✔
438
        }
439
        
440
        /**
441
         * Gets the number of years from a given date up to the current date.
442
         * 
443
         * @param fromDate the date from which to start counting
444
         * @return the number of years
445
         * 
446
         * @since 2.7.8
447
         */
448
        public long getYears(Date fromDate) {
449
                return getYearsBetween(fromDate, new Date());
1✔
450
        }
451
        
452
        /**
453
         * Returns whether the patient is the specified program on the specified date
454
         * 
455
         * @param uuid of program
456
         * @param person the patient to test
457
         * @param onDate the date to test whether the patient is in the program
458
         * @return true if the patient is in the program on the specified date, false otherwise
459
         * 
460
         *  @since 2.8.3
461
         */
462
        public boolean isEnrolledInProgram(String uuid, Person person, Date onDate) {
463
                if (person == null) {
1✔
NEW
464
                        return false;
×
465
                }
466
                if (!(person.getIsPatient())) {
1✔
467
                        return false;
1✔
468
                }
469
                return getPatientPrograms((Patient) person, onDate).stream().anyMatch(pp -> pp.getProgram().getUuid().equals(uuid));
1✔
470
        }
471
        
472
        /**
473
         * Returns whether the patient is the specified program state on the specified date
474
         *
475
         * @param uuid of program state
476
         * @param person  the patient to test
477
         * @param onDate the date to test whether the patient is in the program state
478
         * @return true if the patient is in the program state on the specified date, false otherwise
479
         * 
480
         * @since 2.8.3
481
         */
482
        public boolean isInProgramState(String uuid, Person person, Date onDate) {
483
                if (person == null) {
1✔
NEW
484
                        return false;
×
485
                }
486
                if (!(person.getIsPatient())) {
1✔
487
                        return false;
1✔
488
                }
489
        
490
                List<PatientProgram> patientPrograms = getPatientPrograms((Patient) person, onDate);
1✔
491
                List<PatientState> patientStates = new ArrayList<>();
1✔
492
                
493
                for (PatientProgram pp : patientPrograms) {
1✔
494
                        for (PatientState state : pp.getStates()) {
1✔
495
                                if (state.getActive(onDate)) {
1✔
496
                                        patientStates.add(state);
1✔
497
                                }
498
                        }
1✔
499
                }
1✔
500
                
501
                return patientStates.stream().anyMatch(ps -> ps.getState().getUuid().equals(uuid));
1✔
502
        }
503
        
504
        /**
505
         * Converts a java.util.Date to java.time.LocalDate
506
         * 
507
         * @param date the java.util.Date
508
         * @return the java.time.LocalDate
509
         */
510
        private LocalDate toLocalDate(Date date) {
511
                return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
1✔
512
        }
513
        
514
        private List<PatientProgram> getPatientPrograms(Patient patient, Date onDate) {
515
                if (onDate == null) {
1✔
516
                        onDate = new Date();
×
517
                }
518
                return Context.getProgramWorkflowService().getPatientPrograms(patient, null, null, onDate, onDate, null, false);
1✔
519
        }
520
}
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