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}