001/*
002 * #%L
003 * HAPI FHIR JPA Model
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.model.util;
021
022import java.io.InputStream;
023import java.math.BigDecimal;
024import java.math.MathContext;
025import java.math.RoundingMode;
026
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.rest.param.QuantityParam;
029import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
030import org.fhir.ucum.Decimal;
031import org.fhir.ucum.Pair;
032import org.fhir.ucum.UcumEssenceService;
033import org.fhir.ucum.UcumException;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import ca.uhn.fhir.util.ClasspathUtil;
038
039import javax.annotation.Nullable;
040
041/**
042 * It's a wrapper of UcumEssenceService
043 *
044 */
045public class UcumServiceUtil {
046
047        private static final Logger ourLog = LoggerFactory.getLogger(UcumServiceUtil.class);
048
049        public static final String CELSIUS_CODE = "Cel";
050        public static final String FAHRENHEIT_CODE = "[degF]";
051        public static final float CELSIUS_KELVIN_DIFF = 273.15f;
052
053        public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org";
054        private static final String UCUM_SOURCE = "/ucum-essence.xml";
055
056        private static UcumEssenceService myUcumEssenceService = null;
057
058        private UcumServiceUtil() {
059        }
060
061        // lazy load UCUM_SOURCE only once
062        private static void init() {
063
064                if (myUcumEssenceService != null)
065                        return;
066
067                synchronized (UcumServiceUtil.class) {
068                        InputStream input = ClasspathUtil.loadResourceAsStream(UCUM_SOURCE);
069                        try {
070                                myUcumEssenceService = new UcumEssenceService(input);
071
072                        } catch (UcumException e) {
073                                ourLog.warn("Failed to load ucum code from {}: {}", UCUM_SOURCE, e);
074                        } finally {
075                                ClasspathUtil.close(input);
076                        }
077                }
078        }
079
080        /**
081         * Get the canonical form of a code, it's define at
082         * <link>http://unitsofmeasure.org</link>
083         * 
084         * e.g. 12cm -> 0.12m where m is the canonical form of the length.
085         * 
086         * @param theSystem must be http://unitsofmeasure.org
087         * @param theValue  the value in the original form e.g. 0.12
088         * @param theCode   the code in the original form e.g. 'cm'
089         * @return the CanonicalForm if no error, otherwise return null
090         */
091        public static Pair getCanonicalForm(String theSystem, BigDecimal theValue, String theCode) {
092
093                // -- only for http://unitsofmeasure.org
094                if (!UCUM_CODESYSTEM_URL.equals(theSystem) || theValue == null || theCode == null)
095                        return null;
096
097                if ( isCelsiusOrFahrenheit(theCode) ) {
098                        try {
099                                return getCanonicalFormForCelsiusOrFahrenheit(theValue, theCode);
100                        } catch (UcumException theE) {
101                                ourLog.error("Exception when trying to obtain canonical form for value {} and code {}: {}", theValue, theCode, theE.getMessage());
102                                return null;
103                        }
104                }
105
106                init();
107                Pair theCanonicalPair;
108
109                try {
110                        Decimal theDecimal = new Decimal(theValue.toPlainString(), theValue.precision());
111                        theCanonicalPair = myUcumEssenceService.getCanonicalForm(new Pair(theDecimal, theCode));
112                        // For some reason code [degF], degree Fahrenheit, can't be converted. it returns value null.
113                        if (theCanonicalPair.getValue() == null)
114                                return null;
115                } catch (UcumException e) {
116                        return null;
117                }
118
119                return theCanonicalPair;
120        }
121
122
123        private static Pair getCanonicalFormForCelsiusOrFahrenheit(BigDecimal theValue, String theCode) throws UcumException {
124                return theCode.equals(CELSIUS_CODE)
125                        ? canonicalizeCelsius(theValue)
126                        : canonicalizeFahrenheit(theValue);
127        }
128
129        /**
130         * Returns the received Fahrenheit value converted to Kelvin units and code
131         * Formula is K = (x°F ? 32) × 5/9 + 273.15
132         */
133        private static Pair canonicalizeFahrenheit(BigDecimal theValue) throws UcumException {
134                BigDecimal converted = theValue
135                        .subtract( BigDecimal.valueOf(32) )
136                        .multiply( BigDecimal.valueOf(5f / 9f) )
137                        .add( BigDecimal.valueOf(CELSIUS_KELVIN_DIFF) );
138                // disallow precision larger than input, as it matters when defining ranges
139                BigDecimal adjusted = converted.setScale(theValue.precision(), RoundingMode.HALF_UP);
140
141                Decimal newValue = new Decimal(adjusted.toPlainString());
142                return new Pair(newValue, "K");
143        }
144
145        /**
146         * Returns the received Celsius value converted to Kelvin units and code
147         */
148        private static Pair canonicalizeCelsius(BigDecimal theValue) throws UcumException {
149                Decimal valueDec = new Decimal(theValue.toPlainString(), theValue.precision());
150                Decimal converted = valueDec
151                        .add(new Decimal(Float.toString(CELSIUS_KELVIN_DIFF)));
152
153                return new Pair(converted, "K");
154        }
155
156
157        private static boolean isCelsiusOrFahrenheit(String theCode) {
158                return theCode.equals(CELSIUS_CODE) || theCode.equals(FAHRENHEIT_CODE);
159        }
160
161
162        @Nullable
163    public static QuantityParam toCanonicalQuantityOrNull(QuantityParam theQuantityParam) {
164        Pair canonicalForm = getCanonicalForm(theQuantityParam.getSystem(), theQuantityParam.getValue(), theQuantityParam.getUnits());
165        if (canonicalForm != null) {
166            BigDecimal valueValue = new BigDecimal(canonicalForm.getValue().asDecimal());
167            String unitsValue = canonicalForm.getCode();
168            return new QuantityParam()
169                .setSystem(theQuantityParam.getSystem())
170                .setValue(valueValue)
171                .setUnits(unitsValue)
172                .setPrefix(theQuantityParam.getPrefix());
173        } else {
174            return null;
175        }
176    }
177
178        public static double convert(double theDistanceKm, String theSourceUnits, String theTargetUnits) {
179                init();
180                try {
181                        Decimal distance = new Decimal(Double.toString(theDistanceKm));
182                        Decimal output = myUcumEssenceService.convert(distance, theSourceUnits, theTargetUnits);
183                        String decimal = output.asDecimal();
184                        return Double.parseDouble(decimal);
185                } catch (UcumException e) {
186                        throw new InvalidRequestException(Msg.code(2309) + e.getMessage());
187                }
188        }
189}