
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}