Skip to content

Commit 415fdff

Browse files
authored
Merge branch '2.8.x' into 2.8.x-envers
2 parents 6effc2a + c74fd0c commit 415fdff

File tree

7 files changed

+483
-137
lines changed

7 files changed

+483
-137
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 java.util.Date;
13+
14+
/**
15+
* Holds the context needed to resolve a concept reference range for a given person and concept. The
16+
* optional date field supports retrospective entry scenarios where date-relative criteria (e.g. age
17+
* at encounter time) should be evaluated at a point in time other than today.
18+
*
19+
* @since 3.0.0, 2.9.0, 2.8.5, 2.7.9
20+
*/
21+
public class ConceptReferenceRangeContext {
22+
23+
private final Person person;
24+
25+
private final Concept concept;
26+
27+
private final Date date;
28+
29+
private final Encounter encounter;
30+
31+
private final Obs obs;
32+
33+
/**
34+
* @param person the person to evaluate criteria against (required)
35+
* @param concept the concept whose reference ranges to resolve (required)
36+
* @param date the date at which to evaluate criteria, or null for today
37+
*/
38+
public ConceptReferenceRangeContext(Person person, Concept concept, Date date) {
39+
if (person == null) {
40+
throw new IllegalArgumentException("person is required");
41+
}
42+
if (concept == null) {
43+
throw new IllegalArgumentException("concept is required");
44+
}
45+
this.person = person;
46+
this.concept = concept;
47+
this.date = date;
48+
this.encounter = null;
49+
this.obs = null;
50+
}
51+
52+
/**
53+
* Convenience constructor that extracts person, concept, and obsDatetime from an existing Obs.
54+
* The Obs is retained so that criteria expressions referencing {@code $obs} (e.g.
55+
* {@code $obs.obsDatetime}) continue to work.
56+
*
57+
* @param obs the observation to extract context from
58+
*/
59+
public ConceptReferenceRangeContext(Obs obs) {
60+
if (obs == null) {
61+
throw new IllegalArgumentException("obs is required");
62+
}
63+
if (obs.getPerson() == null) {
64+
throw new IllegalArgumentException("person is required");
65+
}
66+
if (obs.getConcept() == null) {
67+
throw new IllegalArgumentException("concept is required");
68+
}
69+
this.person = obs.getPerson();
70+
this.concept = obs.getConcept();
71+
this.date = obs.getObsDatetime();
72+
this.encounter = obs.getEncounter();
73+
this.obs = obs;
74+
}
75+
76+
/**
77+
* Construct a context from an encounter and concept. The patient and encounter datetime are
78+
* extracted from the encounter.
79+
*
80+
* @param encounter the encounter to extract context from (required)
81+
* @param concept the concept whose reference ranges to resolve (required)
82+
* @since 3.0.0, 2.9.0, 2.8.5, 2.7.9
83+
*/
84+
public ConceptReferenceRangeContext(Encounter encounter, Concept concept) {
85+
if (encounter == null) {
86+
throw new IllegalArgumentException("encounter is required");
87+
}
88+
if (encounter.getPatient() == null) {
89+
throw new IllegalArgumentException("person is required");
90+
}
91+
if (concept == null) {
92+
throw new IllegalArgumentException("concept is required");
93+
}
94+
this.person = encounter.getPatient();
95+
this.concept = concept;
96+
this.date = encounter.getEncounterDatetime();
97+
this.encounter = encounter;
98+
this.obs = null;
99+
}
100+
101+
public Person getPerson() {
102+
return person;
103+
}
104+
105+
public Concept getConcept() {
106+
return concept;
107+
}
108+
109+
/**
110+
* @return the date at which to evaluate criteria, or null meaning "today"
111+
*/
112+
public Date getDate() {
113+
return date;
114+
}
115+
116+
/**
117+
* @return the encounter if this context was constructed from one or from an Obs with an
118+
* encounter, or null
119+
* @since 3.0.0, 2.9.0, 2.8.5, 2.7.9
120+
*/
121+
public Encounter getEncounter() {
122+
return encounter;
123+
}
124+
125+
/**
126+
* @return the original Obs if this context was constructed from one, or null
127+
*/
128+
public Obs getObs() {
129+
return obs;
130+
}
131+
}

api/src/main/java/org/openmrs/api/ConceptService.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.openmrs.ConceptNumeric;
3131
import org.openmrs.ConceptProposal;
3232
import org.openmrs.ConceptReferenceRange;
33+
import org.openmrs.ConceptReferenceRangeContext;
3334
import org.openmrs.ConceptReferenceTerm;
3435
import org.openmrs.ConceptReferenceTermMap;
3536
import org.openmrs.ConceptSearchResult;
@@ -2038,7 +2039,19 @@ public List<ConceptSearchResult> getOrderableConcepts(String phrase, List<Locale
20382039
*/
20392040
@Authorized(PrivilegeConstants.GET_CONCEPTS)
20402041
ConceptReferenceRange getConceptReferenceRange(Person person, Concept concept);
2041-
2042+
2043+
/**
2044+
* Get the appropriate concept reference range for a given context. The context specifies the
2045+
* person, concept, and optionally a date at which to evaluate reference range criteria.
2046+
*
2047+
* @param context the context containing person, concept, and optional date
2048+
* @return the matching ConceptReferenceRange, or null if none found
2049+
*
2050+
* @since 3.0.0, 2.9.0, 2.8.5, 2.7.9
2051+
*/
2052+
@Authorized(PrivilegeConstants.GET_CONCEPTS)
2053+
ConceptReferenceRange getConceptReferenceRange(ConceptReferenceRangeContext context);
2054+
20422055
/**
20432056
* Completely purge a <code>ConceptReferenceRange</code> from the database.
20442057
*

api/src/main/java/org/openmrs/api/impl/ConceptServiceImpl.java

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.UUID;
2626

2727
import org.apache.commons.beanutils.BeanUtils;
28+
import org.apache.commons.text.StringEscapeUtils;
2829
import org.apache.commons.collections.CollectionUtils;
2930
import org.apache.commons.lang3.StringUtils;
3031
import org.apache.commons.lang3.math.NumberUtils;
@@ -44,6 +45,7 @@
4445
import org.openmrs.ConceptNumeric;
4546
import org.openmrs.ConceptProposal;
4647
import org.openmrs.ConceptReferenceRange;
48+
import org.openmrs.ConceptReferenceRangeContext;
4749
import org.openmrs.ConceptReferenceTerm;
4850
import org.openmrs.ConceptReferenceTermMap;
4951
import org.openmrs.ConceptSearchResult;
@@ -65,10 +67,11 @@
6567
import org.openmrs.api.context.Context;
6668
import org.openmrs.api.db.ConceptDAO;
6769
import org.openmrs.api.db.DAOException;
70+
import org.openmrs.api.db.hibernate.HibernateUtil;
6871
import org.openmrs.customdatatype.CustomDatatypeUtil;
72+
import org.openmrs.util.ConceptReferenceRangeUtility;
6973
import org.openmrs.util.OpenmrsConstants;
7074
import org.openmrs.util.OpenmrsUtil;
71-
import org.openmrs.validator.ObsValidator;
7275
import org.openmrs.validator.ValidateUtil;
7376
import org.slf4j.Logger;
7477
import org.slf4j.LoggerFactory;
@@ -2089,8 +2092,126 @@ public ConceptReferenceRange getConceptReferenceRangeByUuid(String uuid) {
20892092

20902093
@Override
20912094
public ConceptReferenceRange getConceptReferenceRange(Person person, Concept concept) {
2092-
Obs obs = new Obs(person, concept, null, null);
2093-
return new ObsValidator().getReferenceRange(obs);
2095+
if (person == null || concept == null) {
2096+
return null;
2097+
}
2098+
return Context.getConceptService().getConceptReferenceRange(
2099+
new ConceptReferenceRangeContext(person, concept, null));
2100+
}
2101+
2102+
@Override
2103+
public ConceptReferenceRange getConceptReferenceRange(ConceptReferenceRangeContext context) {
2104+
if (context == null) {
2105+
throw new IllegalArgumentException("ConceptReferenceRangeContext must not be null");
2106+
}
2107+
2108+
Concept concept = HibernateUtil.getRealObjectFromProxy(context.getConcept());
2109+
if (!(concept instanceof ConceptNumeric) || concept.getDatatype() == null || !concept.getDatatype().isNumeric()) {
2110+
return null;
2111+
}
2112+
ConceptNumeric conceptNumeric = (ConceptNumeric) concept;
2113+
2114+
List<ConceptReferenceRange> referenceRanges =
2115+
Context.getConceptService().getConceptReferenceRangesByConceptId(concept.getConceptId());
2116+
2117+
if (referenceRanges.isEmpty()) {
2118+
return getDefaultReferenceRange(conceptNumeric);
2119+
}
2120+
2121+
ConceptReferenceRangeUtility referenceRangeUtility = new ConceptReferenceRangeUtility();
2122+
List<ConceptReferenceRange> validRanges = new ArrayList<>();
2123+
2124+
for (ConceptReferenceRange referenceRange : referenceRanges) {
2125+
if (referenceRangeUtility.evaluateCriteria(
2126+
StringEscapeUtils.unescapeHtml4(referenceRange.getCriteria()), context)) {
2127+
validRanges.add(referenceRange);
2128+
}
2129+
}
2130+
2131+
if (validRanges.isEmpty()) {
2132+
ConceptReferenceRange defaultReferenceRange = getDefaultReferenceRange(conceptNumeric);
2133+
if (defaultReferenceRange != null) {
2134+
return defaultReferenceRange;
2135+
}
2136+
return null;
2137+
}
2138+
2139+
return findStrictestReferenceRange(validRanges);
2140+
}
2141+
2142+
/**
2143+
* Returns a reference range derived from the ConceptNumeric's own range fields.
2144+
* Used as a fallback when no ConceptReferenceRange records exist or match.
2145+
*/
2146+
private static ConceptReferenceRange getDefaultReferenceRange(ConceptNumeric conceptNumeric) {
2147+
if (conceptNumeric == null || (
2148+
conceptNumeric.getHiAbsolute() == null &&
2149+
conceptNumeric.getHiCritical() == null &&
2150+
conceptNumeric.getHiNormal() == null &&
2151+
conceptNumeric.getLowAbsolute() == null &&
2152+
conceptNumeric.getLowCritical() == null &&
2153+
conceptNumeric.getLowNormal() == null
2154+
)) {
2155+
return null;
2156+
}
2157+
2158+
ConceptReferenceRange defaultReferenceRange = new ConceptReferenceRange();
2159+
defaultReferenceRange.setConceptNumeric(conceptNumeric);
2160+
defaultReferenceRange.setHiAbsolute(conceptNumeric.getHiAbsolute());
2161+
defaultReferenceRange.setHiCritical(conceptNumeric.getHiCritical());
2162+
defaultReferenceRange.setHiNormal(conceptNumeric.getHiNormal());
2163+
defaultReferenceRange.setLowAbsolute(conceptNumeric.getLowAbsolute());
2164+
defaultReferenceRange.setLowCritical(conceptNumeric.getLowCritical());
2165+
defaultReferenceRange.setLowNormal(conceptNumeric.getLowNormal());
2166+
return defaultReferenceRange;
2167+
}
2168+
2169+
/**
2170+
* Combines multiple matching reference ranges into one by selecting the strictest bound for
2171+
* each limit. For low bounds, the highest value is strictest; for high bounds, the lowest.
2172+
* For example, ranges 80-150 and 60-140 combine to 80-140.
2173+
*/
2174+
private static ConceptReferenceRange findStrictestReferenceRange(List<ConceptReferenceRange> conceptReferenceRanges) {
2175+
if (conceptReferenceRanges.size() == 1) {
2176+
return conceptReferenceRanges.get(0);
2177+
}
2178+
2179+
ConceptReferenceRange strictestRange = new ConceptReferenceRange();
2180+
strictestRange.setConceptNumeric(conceptReferenceRanges.get(0).getConceptNumeric());
2181+
2182+
for (ConceptReferenceRange conceptReferenceRange : conceptReferenceRanges) {
2183+
if (conceptReferenceRange.getLowAbsolute() != null &&
2184+
(strictestRange.getLowAbsolute() == null || strictestRange.getLowAbsolute() < conceptReferenceRange.getLowAbsolute())) {
2185+
strictestRange.setLowAbsolute(conceptReferenceRange.getLowAbsolute());
2186+
}
2187+
2188+
if (conceptReferenceRange.getLowCritical() != null &&
2189+
(strictestRange.getLowCritical() == null || strictestRange.getLowCritical() < conceptReferenceRange.getLowCritical())) {
2190+
strictestRange.setLowCritical(conceptReferenceRange.getLowCritical());
2191+
}
2192+
2193+
if (conceptReferenceRange.getLowNormal() != null &&
2194+
(strictestRange.getLowNormal() == null || strictestRange.getLowNormal() < conceptReferenceRange.getLowNormal())) {
2195+
strictestRange.setLowNormal(conceptReferenceRange.getLowNormal());
2196+
}
2197+
2198+
if (conceptReferenceRange.getHiNormal() != null &&
2199+
(strictestRange.getHiNormal() == null || strictestRange.getHiNormal() > conceptReferenceRange.getHiNormal())) {
2200+
strictestRange.setHiNormal(conceptReferenceRange.getHiNormal());
2201+
}
2202+
2203+
if (conceptReferenceRange.getHiCritical() != null &&
2204+
(strictestRange.getHiCritical() == null || strictestRange.getHiCritical() > conceptReferenceRange.getHiCritical())) {
2205+
strictestRange.setHiCritical(conceptReferenceRange.getHiCritical());
2206+
}
2207+
2208+
if (conceptReferenceRange.getHiAbsolute() != null &&
2209+
(strictestRange.getHiAbsolute() == null || strictestRange.getHiAbsolute() > conceptReferenceRange.getHiAbsolute())) {
2210+
strictestRange.setHiAbsolute(conceptReferenceRange.getHiAbsolute());
2211+
}
2212+
}
2213+
2214+
return strictestRange;
20942215
}
20952216

20962217
/***

0 commit comments

Comments
 (0)