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