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

openmrs / openmrs-core / 20760570916

06 Jan 2026 08:05PM UTC coverage: 65.317% (+0.02%) from 65.298%
20760570916

push

github

web-flow
TRUNK-6505: Improve Concept Reference Ranges to allow evaluating base… (#5649)

16 of 17 new or added lines in 1 file covered. (94.12%)

5 existing lines in 2 files now uncovered.

23750 of 36361 relevant lines covered (65.32%)

0.65 hits per line

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

83.78
/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.Obs;
31
import org.openmrs.Patient;
32
import org.openmrs.PatientProgram;
33
import org.openmrs.PatientState;
34
import org.openmrs.Person;
35
import org.openmrs.api.APIException;
36
import org.openmrs.api.context.Context;
37

38
/**
39
 * A utility class that evaluates the concept ranges 
40
 * 
41
 * @since 2.7.0
42
 */
43
public class ConceptReferenceRangeUtility {
44
        
45
        private final long NULL_DATE_RETURN_VALUE = -1;
1✔
46
        
47
        public ConceptReferenceRangeUtility() {
1✔
48
        }
1✔
49
        
50
        /**
51
         * This method evaluates the given criteria against the provided {@link Obs}.
52
         *
53
         * @param criteria the criteria string to evaluate e.g. "$patient.getAge() > 1"
54
         * @param obs The observation (Obs) object containing the values to be used in the criteria evaluation.
55
         *                  
56
         * @return true if the criteria evaluates to true, false otherwise
57
         */
58
        public boolean evaluateCriteria(String criteria, Obs obs) {
59
                if (obs == null) {
1✔
60
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: Obs is null");
1✔
61
                }
62
                
63
                if (obs.getPerson() == null) {
1✔
64
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: patient is null");
×
65
                }
66
                
67
                if (StringUtils.isBlank(criteria)) {
1✔
68
                        throw new IllegalArgumentException("Failed to evaluate criteria with reason: criteria is empty");
1✔
69
                }
70
                
71
                VelocityContext velocityContext = new VelocityContext();
1✔
72
                velocityContext.put("fn", this);
1✔
73
                velocityContext.put("obs", obs);
1✔
74
                
75
                velocityContext.put("patient", obs.getPerson());
1✔
76
                
77
                VelocityEngine velocityEngine = new VelocityEngine();
1✔
78
                try {
79
                        Properties props = new Properties();
1✔
80
                        props.put("runtime.log.logsystem.class", Log4JLogChute.class.getName());
1✔
81
                        props.put("runtime.log.logsystem.log4j.category", "velocity");
1✔
82
                        props.put("runtime.log.logsystem.log4j.logger", "velocity");
1✔
83
                        velocityEngine.init(props);
1✔
84
                }
85
                catch (Exception e) {
×
86
                        throw new APIException("Failed to create the velocity engine: " + e.getMessage(), e);
×
87
                }
1✔
88
                
89
                StringWriter writer = new StringWriter();
1✔
90
                String wrappedCriteria = "#set( $criteria = " + criteria + " )$criteria";
1✔
91
                
92
                try {
93
                        velocityEngine.evaluate(velocityContext, writer, ConceptReferenceRangeUtility.class.getName(), wrappedCriteria);
1✔
94
                        return Boolean.parseBoolean(writer.toString());
1✔
95
                }
96
                catch (ParseErrorException e) {
1✔
97
                        throw new APIException("An error occurred while evaluating criteria. Invalid criteria: " + criteria, e);
1✔
98
                }
99
                catch (Exception e) {
×
100
                        throw new APIException("An error occurred while evaluating criteria: ", e);
×
101
                }
102
        }
103
        
104
        /**
105
         * Gets the latest Obs by concept.
106
         *
107
         * @param conceptRef can be either concept uuid or conceptMap's code and sourceName 
108
         *                   e.g "bac25fd5-c143-4e43-bffe-4eb1e7efb6ce" or "CIEL:1434"
109
         * @param person person to get obs for
110
         *                   
111
         * @return Obs latest Obs
112
         */
113
        public Obs getLatestObs(String conceptRef, Person person) {
114
                Concept concept = Context.getConceptService().getConceptByReference(conceptRef);
1✔
115

116
                if (concept != null) {
1✔
117
                        List<Obs> observations = Context.getObsService().getObservations(
1✔
118
                                Collections.singletonList(person), 
1✔
119
                                null, 
120
                                Collections.singletonList(concept), 
1✔
121
                                null, 
122
                                null, 
123
                                null,
124
                                Collections.singletonList("dateCreated"), 
1✔
125
                                1, 
1✔
126
                                null,
127
                                null, 
128
                                null, 
129
                                false
130
                        );
131

132
                        return observations.isEmpty() ? null : observations.get(0);
1✔
133
                }
134

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