001package org.hl7.fhir.utilities.ucum;
002
003/*******************************************************************************
004 * Crown Copyright (c) 2006 - 2014, Copyright (c) 2006 - 2014 Kestral Computing & Health Intersections.
005 * All rights reserved. This program and the accompanying materials
006 * are made available under the terms of the Eclipse Public License v1.0
007 * which accompanies this distribution, and is available at
008 * http://www.eclipse.org/legal/epl-v10.html
009 * 
010 * Contributors:
011 *    Kestral Computing P/L - initial implementation
012 *    Health Intersections - ongoing maintenance
013 *******************************************************************************/
014
015import java.io.File;
016import java.io.InputStream;
017import java.util.ArrayList;
018import java.util.HashSet;
019import java.util.List;
020import java.util.Set;
021
022import org.hl7.fhir.exceptions.UcumException;
023import org.hl7.fhir.utilities.Utilities;
024import org.hl7.fhir.utilities.ucum.special.Registry;
025
026
027/**
028 * implements UCUM services. Applications must provide a copy of 
029 * ucum-essence.xml as either a stream or a file name to create 
030 * the services.
031 * 
032 * the provided ucum-essence.xml must be released on 25 Apr 2008 
033 * or more recent. Note that if the ucum-essence.xml file does not 
034 * contain a release date on an attribute of the root element, it 
035 * is not more recent than this date (Gunther added it on this date for 
036 * this usage, thanks)
037 * 
038 * See UcumService for documentation concerning the services this class provides
039 * 
040 * @author Grahame Grieve
041 *
042 */
043public class UcumEssenceService implements UcumService {
044
045        public static final String UCUM_OID = "2.16.840.1.113883.6.8";
046        
047        private UcumModel model;
048        private Registry handlers = new Registry();
049        
050        /**
051         * Create an instance of Ucum services. Stream must point to a
052         * valid ucum-essence file (see class documentation) 
053         * @throws UcumException 
054         */
055        public UcumEssenceService(InputStream stream) throws UcumException  {
056                super();
057                assert stream != null : paramError("factory", "stream", "must not be null");
058                try {
059                        model = new DefinitionParser().parse(stream);
060                } catch (Exception e) {
061                        throw new UcumException(e); 
062                }
063        }
064
065        /**
066         * Create an instance of Ucum services. filename must point to a
067         * valid ucum-essence file (see class documentation)    
068         * @throws UcumException 
069         */
070        public UcumEssenceService(String filename) throws UcumException  {
071                super();
072                assert new File(filename).exists() : paramError("factory", "file", "must exist");
073                try {
074                        model = new DefinitionParser().parse(filename);
075                } catch (Exception e) {
076                        throw new UcumException(e); 
077                }
078        }
079
080        private String paramError(String method, String param, String msg) {
081                return getClass().getName()+"."+method+"."+param+" is not acceptable: "+msg;
082        }
083
084
085        @Override
086  public UcumVersionDetails ucumIdentification() {
087                return new UcumVersionDetails(model.getRevisionDate(), model.getVersion());
088        }
089        
090        
091        /* (non-Javadoc)
092         * @see org.eclipse.ohf.ucum.UcumServiceEx#getModel()
093         */
094        @Override
095  public UcumModel getModel() {
096                return model;
097        }
098        
099        
100        /* (non-Javadoc)
101         * @see org.eclipse.ohf.ucum.UcumServiceEx#search(org.eclipse.ohf.ucum.model.ConceptKind, java.lang.String, boolean)
102         */
103        @Override
104  public List<Concept> search(ConceptKind kind, String text, boolean isRegex) {
105                assert checkStringParam(text) : paramError("search", "text", "must not be null or empty");
106                return new Search().doSearch(model, kind, text, isRegex);
107        }
108
109
110        /* (non-Javadoc)
111         * @see org.eclipse.ohf.ucum.UcumServiceEx#validateUCUM()
112         */
113        @Override
114  public List<String> validateUCUM() {          
115                return new UcumValidator(model, handlers).validate();           
116        }
117
118        /* (non-Javadoc)
119         * @see org.eclipse.ohf.ucum.UcumServiceEx#getProperties()
120         */
121        @Override
122  public Set<String> getProperties() {
123                Set<String> result = new HashSet<String>();
124                for (DefinedUnit unit : model.getDefinedUnits()) {
125                        result.add(unit.getProperty());
126                }
127                return result;          
128        }
129        
130        /* (non-Javadoc)
131         * @see org.eclipse.ohf.ucum.UcumServiceEx#validate(java.lang.String)
132         */
133        @Override
134  public String validate(String unit) {
135                assert unit != null : paramError("validate", "unit", "must not be null");
136                try {
137                        new ExpressionParser(model).parse(unit);
138                        return null;
139                } catch (Exception e) {
140                        return e.getMessage();
141                }
142        }
143
144        /* (non-Javadoc)
145         * @see org.eclipse.ohf.ucum.UcumServiceEx#validateInProperty(java.lang.String, java.lang.String)
146         */
147        @Override
148  public String validateInProperty(String unit, String property) {
149                assert checkStringParam(unit) : paramError("validate", "unit", "must not be null or empty");
150                assert checkStringParam(property) : paramError("validateInProperty", "property", "must not be null or empty");
151                try {
152                        Term term = new ExpressionParser(model).parse(unit);
153                        Canonical can = new Converter(model, handlers).convert(term);
154                        String cu = new ExpressionComposer().compose(can, false);
155                        if (can.getUnits().size() == 1) {
156                                        if (property.equals(can.getUnits().get(0).getBase().getProperty()))
157                                                return null;
158                                        else
159                                                return "unit "+unit+" is of the property type "+can.getUnits().get(0).getBase().getProperty()+" ("+cu+"), not "+property+" as required.";
160                        }
161                        // defined special case
162                        if ("concentration".equals(property) && ("g/L".equals(cu) || "mol/L".equals(cu)))
163                                return null;
164                        
165                        return "unit "+unit+" has the base units "+cu+", and are not from the property "+property+" as required.";
166                } catch (Exception e) {
167                        return e.getMessage();
168                }
169        }
170
171        /* (non-Javadoc)
172         * @see org.eclipse.ohf.ucum.UcumServiceEx#validateCanonicalUnits(java.lang.String, java.lang.String)
173         */
174        @Override
175  public String validateCanonicalUnits(String unit, String canonical) {
176                assert checkStringParam(unit) : paramError("validate", "unit", "must not be null or empty");
177                assert checkStringParam(canonical) : paramError("validateCanonicalUnits", "canonical", "must not be null or empty");
178                try {
179                        Term term = new ExpressionParser(model).parse(unit);
180                        Canonical can = new Converter(model, handlers).convert(term);
181                        String cu = new ExpressionComposer().compose(can, false);
182                        if (!canonical.equals(cu))
183                                return "unit "+unit+" has the base units "+cu+", not "+canonical+" as required.";
184                        return null;
185                } catch (Exception e) {
186                        return e.getMessage();
187                }
188        }
189
190        /**
191         * given a unit, return a formal description of what the units stand for using
192         * full names 
193         * @param units the unit code
194         * @return formal description
195         * @throws UcumException 
196         * @ 
197         */
198        @Override
199  public String analyse(String unit) throws UcumException  {
200                if (Utilities.noString(unit))
201                        return "(unity)";
202                assert checkStringParam(unit) : paramError("analyse", "unit", "must not be null or empty");
203                Term term = new ExpressionParser(model).parse(unit);
204                return new FormalStructureComposer().compose(term);
205        }
206        
207        /* (non-Javadoc)
208         * @see org.eclipse.ohf.ucum.UcumServiceEx#getCanonicalUnits(java.lang.String)
209         */
210        @Override
211  public String getCanonicalUnits(String unit) throws UcumException  {
212                assert checkStringParam(unit) : paramError("getCanonicalUnits", "unit", "must not be null or empty");
213                try {
214                        Term term = new ExpressionParser(model).parse(unit);
215                        return new ExpressionComposer().compose(new Converter(model, handlers).convert(term), false);   
216                } catch (Exception e) {
217                        throw new UcumException("Error processing "+unit+": "+e.getMessage(), e);
218                }
219        }
220        
221        /* (non-Javadoc)
222         * @see org.eclipse.ohf.ucum.UcumServiceEx#getDefinedForms(java.lang.String)
223         */
224        @Override
225  public List<DefinedUnit> getDefinedForms(String code) throws UcumException  {
226                assert checkStringParam(code) : paramError("getDefinedForms", "code", "must not be null or empty");
227                List<DefinedUnit> result = new ArrayList<DefinedUnit>(); 
228                BaseUnit base = model.getBaseUnit(code);
229                if (base != null) {
230                        for (DefinedUnit unit : model.getDefinedUnits()) {
231                                if (!unit.isSpecial() && code.equals(getCanonicalUnits(unit.getCode())))
232                                        result.add(unit);
233                        }
234                }               
235                return result;
236        }
237        
238        private boolean checkStringParam(String s) {
239                return s != null && !s.equals("");
240        }
241
242        
243        /* (non-Javadoc)
244         * @see org.eclipse.ohf.ucum.UcumServiceEx#getCanonicalForm(org.eclipse.ohf.ucum.UcumEssenceService.Pair)
245         */
246        @Override
247  public Pair getCanonicalForm(Pair value) throws UcumException  {
248                assert value != null : paramError("getCanonicalForm", "value", "must not be null");
249                assert checkStringParam(value.getCode()) : paramError("getCanonicalForm", "value.code", "must not be null or empty");
250                
251                Term term = new ExpressionParser(model).parse(value.getCode());
252                Canonical c = new Converter(model, handlers).convert(term);
253                if (value.getValue() == null)
254                        return new Pair(null, new ExpressionComposer().compose(c, false));
255                else
256                        return new Pair(value.getValue().multiply(c.getValue()), new ExpressionComposer().compose(c, false));
257        }
258        
259        /* (non-Javadoc)
260         * @see org.eclipse.ohf.ucum.UcumServiceEx#convert(java.math.BigDecimal, java.lang.String, java.lang.String)
261         */
262        @Override
263  public Decimal convert(Decimal value, String sourceUnit, String destUnit) throws UcumException  {
264                assert value != null : paramError("convert", "value", "must not be null");
265                assert checkStringParam(sourceUnit) : paramError("convert", "sourceUnit", "must not be null or empty");
266                assert checkStringParam(destUnit) : paramError("convert", "destUnit", "must not be null or empty");
267
268                if (sourceUnit.equals(destUnit))
269                        return value;
270                        
271                Canonical src = new Converter(model, handlers).convert(new ExpressionParser(model).parse(sourceUnit));
272                Canonical dst = new Converter(model, handlers).convert(new ExpressionParser(model).parse(destUnit));
273                String s = new ExpressionComposer().compose(src, false);
274                String d = new ExpressionComposer().compose(dst, false);
275                if (!s.equals(d))
276                        throw new UcumException("Unable to convert between units "+sourceUnit+" and "+destUnit+" as they do not have matching canonical forms ("+s+" and "+d+" respectively)");
277                Decimal canValue = value.multiply(src.getValue());
278//              System.out.println(value.toPlainString()+sourceUnit+" =("+src.getValue().toPlainString()+")= "+
279//                              canValue.toPlainString()+s+" =("+dst.getValue().toPlainString()+")= "+
280//                              canValue.divide(dst.getValue())+destUnit);
281                return canValue.divide(dst.getValue());
282        }
283
284        @Override
285  public Pair multiply(Pair o1, Pair o2) throws UcumException  {
286          Pair res = new Pair(o1.getValue().multiply(o2.getValue()), o1.getCode() +"."+o2.getCode());
287          return getCanonicalForm(res);
288        }
289
290        @Override
291  public String getCommonDisplay(String code) {
292                //TODO: improvements
293          return code.replace("[", "").replace("]", "");
294  }
295
296  @Override
297  public boolean isComparable(String units1, String units2) throws UcumException  {
298    if (units1 == null)
299      return false;
300    if (units2 == null)
301      return false;
302    
303    String u1 = getCanonicalUnits(units1);
304    String u2 = getCanonicalUnits(units2);
305    return u1.equals(u2);
306  }
307
308}