• 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

91.57
/api/src/main/java/org/openmrs/validator/ObsValidator.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.validator;
11

12
import java.util.ArrayList;
13
import java.util.List;
14
import java.util.Set;
15

16
import org.openmrs.Concept;
17
import org.openmrs.ConceptDatatype;
18
import org.openmrs.ConceptNumeric;
19
import org.openmrs.ConceptReferenceRange;
20
import org.openmrs.ConceptReferenceRangeContext;
21
import org.openmrs.Obs;
22
import org.openmrs.ObsReferenceRange;
23
import org.openmrs.annotation.Handler;
24
import org.openmrs.api.APIException;
25
import org.openmrs.api.context.Context;
26
import org.springframework.validation.Errors;
27
import org.springframework.validation.Validator;
28

29
/**
30
 * Validator for the Obs class. This class checks for anything set on the Obs object that will cause
31
 * errors or is incorrect. Things checked are similar to:
32
 * <ul>
33
 * <li>all required properties are filled in on the Obs object.
34
 * <li>checks for no recursion in the obs grouping.
35
 * <li>Makes sure the obs has at least one value (if not an obs grouping)</li>
36
 * </ul>
37
 * 
38
 * @see org.openmrs.Obs
39
 */
40
@Handler(supports = { Obs.class }, order = 50)
41
public class ObsValidator implements Validator {
1✔
42
        
43
        public static final int VALUE_TEXT_MAX_LENGTH = 65535;
44
        
45
        /**
46
         * @see org.springframework.validation.Validator#supports(java.lang.Class)
47
         * <strong>Should</strong> support Obs class
48
         */
49
        @Override
50
        public boolean supports(Class<?> c) {
51
                return Obs.class.isAssignableFrom(c);
1✔
52
        }
53
        
54
        /**
55
         * @see org.springframework.validation.Validator#validate(java.lang.Object,
56
         *      org.springframework.validation.Errors)
57
         * <strong>Should</strong> fail validation if personId is null
58
         * <strong>Should</strong> fail validation if obsDatetime is null
59
         * <strong>Should</strong> fail validation if concept is null
60
         * <strong>Should</strong> fail validation if concept datatype is boolean and valueBoolean is null
61
         * <strong>Should</strong> fail validation if concept datatype is coded and valueCoded is null
62
         * <strong>Should</strong> fail validation if concept datatype is date and valueDatetime is null
63
         * <strong>Should</strong> fail validation if concept datatype is numeric and valueNumeric is null
64
         * <strong>Should</strong> fail validation if concept datatype is text and valueText is null
65
         * <strong>Should</strong> fail validation if obs ancestors contains obs
66
         * <strong>Should</strong> pass validation if all values present
67
         * <strong>Should</strong> fail validation if the parent obs has values
68
         * <strong>Should</strong> reject an invalid concept and drug combination
69
         * <strong>Should</strong> pass if answer concept and concept of value drug match
70
         * <strong>Should</strong> pass validation if field lengths are correct
71
         * <strong>Should</strong> fail validation if field lengths are not correct
72
         * <strong>Should</strong> not validate if obs is voided
73
         * <strong>Should</strong> not validate a voided child obs
74
         * <strong>Should</strong> fail for a null object
75
         */
76
        @Override
77
        public void validate(Object obj, Errors errors) {
78
                Obs obs = (Obs) obj;
1✔
79
                if (obs == null) {
1✔
80
                        throw new APIException("Obs can't be null");
1✔
81
                } else if (obs.getVoided()) {
1✔
82
                        return;
1✔
83
                }
84
                List<Obs> ancestors = new ArrayList<>();
1✔
85
                validateHelper(obs, errors, ancestors, true);
1✔
86
                ValidateUtil.validateFieldLengths(errors, obj.getClass(), "accessionNumber", "valueModifier", "valueComplex",
1✔
87
                    "comment", "voidReason");
88
        }
1✔
89
        
90
        /**
91
         * Checks whether obs has all required values, and also checks to make sure that no obs group
92
         * contains any of its ancestors
93
         *
94
         * @param obs
95
         * @param errors
96
         * @param ancestors
97
         * @param atRootNode whether or not this is the obs that validate() was originally called on. If
98
         *            not then we shouldn't reject fields by name.
99
         */
100
        private void validateHelper(Obs obs, Errors errors, List<Obs> ancestors, boolean atRootNode) {
101
                if (obs.getPersonId() == null) {
1✔
102
                        errors.rejectValue("person", "error.null");
1✔
103
                }
104
                if (obs.getObsDatetime() == null) {
1✔
105
                        errors.rejectValue("obsDatetime", "error.null");
1✔
106
                }
107
                
108
                boolean isObsGroup = obs.hasGroupMembers(true);
1✔
109
                // if this is an obs group (i.e., parent) make sure that it has no values (other than valueGroupId) set
110
                if (isObsGroup) {
1✔
111
                        if (obs.getValueCoded() != null) {
1✔
112
                                errors.rejectValue("valueCoded", "error.not.null");
1✔
113
                        }
114
                        
115
                        if (obs.getValueDrug() != null) {
1✔
116
                                errors.rejectValue("valueDrug", "error.not.null");
1✔
117
                        }
118
                        
119
                        if (obs.getValueDatetime() != null) {
1✔
120
                                errors.rejectValue("valueDatetime", "error.not.null");
1✔
121
                        }
122
                        
123
                        if (obs.getValueNumeric() != null) {
1✔
124
                                errors.rejectValue("valueNumeric", "error.not.null");
1✔
125
                        }
126
                        
127
                        if (obs.getValueModifier() != null) {
1✔
128
                                errors.rejectValue("valueModifier", "error.not.null");
1✔
129
                        }
130
                        
131
                        if (obs.getValueText() != null) {
1✔
132
                                errors.rejectValue("valueText", "error.not.null");
1✔
133
                        }
134
                        
135
                        if (obs.getValueBoolean() != null) {
1✔
136
                                errors.rejectValue("valueBoolean", "error.not.null");
1✔
137
                        }
138
                        
139
                        if (obs.getValueComplex() != null) {
1✔
140
                                errors.rejectValue("valueComplex", "error.not.null");
1✔
141
                        }
142
                        
143
                }
144
                // if this is NOT an obs group, make sure that it has at least one value set (not counting obsGroupId)
145
                else if (obs.getValueBoolean() == null && obs.getValueCoded() == null && obs.getValueCodedName() == null
1✔
146
                        && obs.getValueComplex() == null && obs.getValueDatetime() == null && obs.getValueDrug() == null
1✔
147
                        && obs.getValueModifier() == null && obs.getValueNumeric() == null && obs.getValueText() == null
1✔
148
                        && obs.getComplexData() == null) {
1✔
149
                        errors.reject("error.noValue");
1✔
150
                }
151
                
152
                // make sure there is a concept associated with the obs
153
                Concept c = obs.getConcept();
1✔
154
                if (c == null) {
1✔
155
                        errors.rejectValue("concept", "error.null");
1✔
156
                }
157
                // if there is a concept, and this isn't a group, perform validation tests specific to the concept datatype
158
                else if (!isObsGroup) {
1✔
159
                        ConceptDatatype dt = c.getDatatype();
1✔
160
                        if (dt != null) {
1✔
161
                                if (dt.isBoolean() && obs.getValueBoolean() == null) {
1✔
162
                                        if (atRootNode) {
1✔
163
                                                errors.rejectValue("valueBoolean", "error.null");
1✔
164
                                        } else {
165
                                                errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
166
                                        }
167
                                } else if (dt.isCoded() && obs.getValueCoded() == null) {
1✔
168
                                        if (atRootNode) {
1✔
169
                                                errors.rejectValue("valueCoded", "error.null");
1✔
170
                                        } else {
171
                                                errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
172
                                        }
173
                                } else if ((dt.isDateTime() || dt.isDate() || dt.isTime()) && obs.getValueDatetime() == null) {
1✔
174
                                        if (atRootNode) {
1✔
175
                                                errors.rejectValue("valueDatetime", "error.null");
1✔
176
                                        } else {
177
                                                errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
178
                                        }
179
                                } else if (dt.isNumeric() && obs.getValueNumeric() == null) {
1✔
180
                                        if (atRootNode) {
1✔
181
                                                errors.rejectValue("valueNumeric", "error.null");
1✔
182
                                        } else {
183
                                                errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
184
                                        }
185
                                } else if (dt.isNumeric()) {
1✔
186
                                        ConceptNumeric cn = Context.getConceptService().getConceptNumeric(c.getConceptId());
1✔
187
                                        // If the concept numeric is not precise, the value cannot be a float, so raise an error 
188
                                        if (!cn.getAllowDecimal() && Math.ceil(obs.getValueNumeric()) != obs.getValueNumeric()) {
1✔
189
                                                if (atRootNode) {
×
190
                                                        errors.rejectValue("valueNumeric", "Obs.error.precision");
×
191
                                                } else {
192
                                                        errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
193
                                                }
194
                                        }
195
                                        
196
                                        validateConceptReferenceRange(obs, errors, atRootNode);
1✔
197
                                } else if (dt.isText() && obs.getValueText() == null) {
1✔
198
                                        if (atRootNode) {
1✔
199
                                                errors.rejectValue("valueText", "error.null");
1✔
200
                                        } else {
201
                                                errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
202
                                        }
203
                                }
204
                                
205
                                //If valueText is longer than the maxlength, raise an error as well.
206
                                if (dt.isText() && obs.getValueText() != null && obs.getValueText().length() > VALUE_TEXT_MAX_LENGTH) {
1✔
207
                                        if (atRootNode) {
1✔
208
                                                errors.rejectValue("valueText", "error.exceededMaxLengthOfField");
1✔
209
                                        } else {
210
                                                errors.rejectValue("groupMembers", "Obs.error.inGroupMember");
×
211
                                        }
212
                                }
213
                        } else { // dt is null
214
                                errors.rejectValue("concept", "must have a datatype");
×
215
                        }
216
                }
217
                
218
                // If an obs fails validation, don't bother checking its children
219
                if (errors.hasErrors()) {
1✔
220
                        return;
1✔
221
                }
222
                
223
                if (ancestors.contains(obs)) {
1✔
224
                        errors.rejectValue("groupMembers", "Obs.error.groupContainsItself");
1✔
225
                }
226
                
227
                Set<Obs> groupMembers = obs.getGroupMembers();
1✔
228
                if (groupMembers != null && !groupMembers.isEmpty()) {
1✔
229
                        ancestors.add(obs);
1✔
230
                        for (Obs child : groupMembers) {
1✔
231
                                validateHelper(child, errors, ancestors, false);
1✔
232
                        }
1✔
233
                        ancestors.remove(ancestors.size() - 1);
1✔
234
                }
235
                
236
                if (obs.getValueCoded() != null && obs.getValueDrug() != null && obs.getValueDrug().getConcept() != null) {
1✔
237
                        Concept trueConcept = Context.getConceptService().getTrueConcept();
1✔
238
                        Concept falseConcept = Context.getConceptService().getFalseConcept();
1✔
239
                        //Ignore if this is not a true or false response since they are stored as coded too
240
                        if (!obs.getValueCoded().equals(trueConcept) && !obs.getValueCoded().equals(falseConcept)
1✔
241
                                && !obs.getValueDrug().getConcept().equals(obs.getValueCoded())) {
1✔
242
                                errors.rejectValue("valueDrug", "Obs.error.invalidDrug");
1✔
243
                        }
244
                }
245
        }
1✔
246

247
        /**
248
         * This method validates Obs' numeric values:
249
         * <ol>
250
         *     <li>Validates Obs in relation to criteria e.g. checks patient's age is within the valid range</li>
251
         *     <li>Validates if Obs' numeric value is within the valid range; i.e. >= low absolute && <= high absolute.</li>
252
         *     <li>Sets field errors if numeric value is outside the valid range</li>
253
         * <ol/>
254
         *
255
         * @param obs Observation to validate
256
         * @param errors Errors to record validation issues
257
         */
258
        private void validateConceptReferenceRange(Obs obs, Errors errors, boolean atRootNode) {
259
                ConceptReferenceRange conceptReferenceRange = getReferenceRange(obs);
1✔
260

261
                if (conceptReferenceRange != null) {
1✔
262
                        validateAbsoluteRanges(obs, conceptReferenceRange, errors, atRootNode);
1✔
263
                        
264
                        if (obs.getId() == null) {
1✔
265
                                setObsReferenceRange(obs, conceptReferenceRange);
1✔
266
                        }
267
                } else if (obs.getId() == null) {
1✔
268
                        setObsReferenceRange(obs);
1✔
269
                }
270
                setObsInterpretation(obs);
1✔
271
        }
1✔
272

273
        /**
274
         * Evaluates the criteria and return the most strict {@link ConceptReferenceRange} for a given concept
275
         * and patient contained in an observation.
276
         * It considers all valid ranges that match the criteria for the person.
277
         *
278
         * @param obs containing The concept and patient for whom the range is being evaluated
279
         * @return The strictest {@link ConceptReferenceRange}, or null if no valid range is found
280
         * 
281
         * @since 2.7.0
282
         */
283
        public ConceptReferenceRange getReferenceRange(Obs obs) {
284
                if (obs == null || obs.getPerson() == null || obs.getConcept() == null) {
1✔
UNCOV
285
                        return null;
×
286
                }
287
                return Context.getConceptService().getConceptReferenceRange(
1✔
288
                        new ConceptReferenceRangeContext(obs));
289
        }
290

291
        /**
292
         * Validates the high and low absolute values of the Obs.
293
         *
294
         * @param obs Observation to validate
295
         * @param conceptReferenceRange ConceptReferenceRange containing the range values
296
         * @param errors Errors to record validation issues
297
         */
298
        private void validateAbsoluteRanges(Obs obs, ConceptReferenceRange conceptReferenceRange, Errors errors, boolean atRootNode) {
299
                if (conceptReferenceRange.getHiAbsolute() != null && conceptReferenceRange.getHiAbsolute() < obs.getValueNumeric()) {
1✔
300
                        if (atRootNode) {
1✔
301
                                errors.rejectValue(
1✔
302
                                        "valueNumeric",
303
                                        "error.value.outOfRange.high",
304
                                        new Object[] { conceptReferenceRange.getHiAbsolute() },
1✔
305
                                        null
306
                                );
307
                        } else {
308
                                errors.rejectValue(
×
309
                                        "groupMembers",
310
                                        "Obs.error.inGroupMember",
311
                                        new Object[] {},
312
                                        null
313
                                );
314
                        }
315
                }
316
                
317
                if (conceptReferenceRange.getLowAbsolute() != null && conceptReferenceRange.getLowAbsolute() > obs.getValueNumeric()) {
1✔
318
                        if (atRootNode) {
1✔
319
                                errors.rejectValue(
1✔
320
                                        "valueNumeric",
321
                                        "error.value.outOfRange.low",
322
                                        new Object[] { conceptReferenceRange.getLowAbsolute() },
1✔
323
                                        null
324
                                );
325
                        } else {
326
                                errors.rejectValue(
×
327
                                        "groupMembers",
328
                                        "Obs.error.inGroupMember",
329
                                        new Object[] { },
330
                                        null
331
                                );
332
                        }
333
                }
334
        }
1✔
335

336
        /**
337
         * Builds and sets the ObsReferenceRange for the given Obs.
338
         *
339
         * @param obs Observation to set the reference range
340
         * @param conceptReferenceRange ConceptReferenceRange used to build the ObsReferenceRange
341
         */
342
        private void setObsReferenceRange(Obs obs, ConceptReferenceRange conceptReferenceRange) {
343
                ObsReferenceRange obsRefRange = new ObsReferenceRange();
1✔
344

345
                obsRefRange.setHiAbsolute(conceptReferenceRange.getHiAbsolute());
1✔
346
                obsRefRange.setHiCritical(conceptReferenceRange.getHiCritical());
1✔
347
                obsRefRange.setHiNormal(conceptReferenceRange.getHiNormal());
1✔
348
                obsRefRange.setLowAbsolute(conceptReferenceRange.getLowAbsolute());
1✔
349
                obsRefRange.setLowCritical(conceptReferenceRange.getLowCritical());
1✔
350
                obsRefRange.setLowNormal(conceptReferenceRange.getLowNormal());
1✔
351
                obsRefRange.setObs(obs);
1✔
352

353
                obs.setReferenceRange(obsRefRange);
1✔
354
        }
1✔
355

356
        /**
357
         * Builds and sets the ObsReferenceRange from concept numeric values.
358
         *
359
         * @param obs Observation to set the reference range
360
         */
361
        private void setObsReferenceRange(Obs obs) {
362
                if (obs.getConcept() == null) {
1✔
363
                        return;
×
364
                }
365
                
366
                ConceptNumeric conceptNumeric = Context.getConceptService().getConceptNumeric(obs.getConcept().getId());
1✔
367

368
                if (conceptNumeric != null) {
1✔
369
                        ObsReferenceRange obsRefRange = new ObsReferenceRange();
1✔
370

371
                        obsRefRange.setHiAbsolute(conceptNumeric.getHiAbsolute());
1✔
372
                        obsRefRange.setHiCritical(conceptNumeric.getHiCritical());
1✔
373
                        obsRefRange.setHiNormal(conceptNumeric.getHiNormal());
1✔
374
                        obsRefRange.setLowAbsolute(conceptNumeric.getLowAbsolute());
1✔
375
                        obsRefRange.setLowCritical(conceptNumeric.getLowCritical());
1✔
376
                        obsRefRange.setLowNormal(conceptNumeric.getLowNormal());
1✔
377
                        obsRefRange.setObs(obs);
1✔
378
                        
379
                        obs.setReferenceRange(obsRefRange);
1✔
380
                }
381
        }
1✔
382

383
        /**
384
         * This method sets Obs interpretation based on the current obs' numeric value.
385
         *
386
         * @param obs Observation to set the interpretation
387
         */
388
        private void setObsInterpretation(Obs obs) {
389
                ObsReferenceRange referenceRange = obs.getReferenceRange();
1✔
390
                if (referenceRange == null || obs.getValueNumeric() == null) {
1✔
391
                        return;
1✔
392
                }
393
                
394
                if (referenceRange.getHiNormal() != null 
1✔
395
                        && referenceRange.getHiCritical() != null
1✔
396
                        && obs.getValueNumeric() > referenceRange.getHiNormal()
1✔
397
                        && obs.getValueNumeric() < referenceRange.getHiCritical()) {
1✔
398
                        obs.setInterpretation(Obs.Interpretation.HIGH);
1✔
399
                } else if (referenceRange.getHiCritical() != null 
1✔
400
                        && obs.getValueNumeric() >= referenceRange.getHiCritical()) {
1✔
401
                        obs.setInterpretation(Obs.Interpretation.CRITICALLY_HIGH);
1✔
402
                } else if (referenceRange.getLowNormal() != null 
1✔
403
                        && referenceRange.getLowCritical() != null
1✔
404
                        && obs.getValueNumeric() < referenceRange.getLowNormal() 
1✔
405
                        && obs.getValueNumeric() > referenceRange.getLowCritical()) {
1✔
406
                        obs.setInterpretation(Obs.Interpretation.LOW);
1✔
407
                } else if (referenceRange.getLowNormal() != null 
1✔
408
                        && referenceRange.getHiNormal() != null
1✔
409
                        && obs.getValueNumeric() >= referenceRange.getLowNormal() 
1✔
410
                        && obs.getValueNumeric() <= referenceRange.getHiNormal()) {
1✔
411
                        obs.setInterpretation(Obs.Interpretation.NORMAL);
1✔
412
                } else if (referenceRange.getLowCritical() != null 
1✔
413
                        && obs.getValueNumeric() <= referenceRange.getLowCritical()) {
1✔
414
                        obs.setInterpretation(Obs.Interpretation.CRITICALLY_LOW);
1✔
415
                }
416
        }
1✔
417
        
418
}
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