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