001/*
002 * Units of Measurement Systems
003 * Copyright (c) 2005-2017, Jean-Marie Dautelle, Werner Keil and others.
004 *
005 * All rights reserved.
006 *
007 * Redistribution and use in source and binary forms, with or without modification,
008 * are permitted provided that the following conditions are met:
009 *
010 * 1. Redistributions of source code must retain the above copyright notice,
011 *    this list of conditions and the following disclaimer.
012 *
013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
014 *    and the following disclaimer in the documentation and/or other materials provided with the distribution.
015 *
016 * 3. Neither the name of JSR-363, Units of Measurement nor the names of their contributors may be used to
017 *    endorse or promote products derived from this software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package systems.uom.ucum.format;
031
032import static systems.uom.ucum.format.UCUMConverterFormatter.formatConverter;
033import static tec.uom.se.AbstractUnit.ONE;
034import si.uom.SI;
035import systems.uom.ucum.internal.format.UCUMFormatParser;
036import tec.uom.se.AbstractUnit;
037import tec.uom.se.format.AbstractUnitFormat;
038import tec.uom.se.format.SymbolMap;
039import tec.uom.se.internal.format.TokenException;
040import tec.uom.se.internal.format.TokenMgrError;
041import tec.uom.se.unit.AnnotatedUnit;
042import tec.uom.se.unit.MetricPrefix;
043import tec.uom.se.unit.TransformedUnit;
044
045import javax.measure.Quantity;
046import javax.measure.Unit;
047import javax.measure.UnitConverter;
048import javax.measure.format.ParserException;
049
050import java.io.ByteArrayInputStream;
051import java.io.IOException;
052import java.text.ParsePosition;
053import java.util.*;
054
055/**
056 * <p>
057 * This class provides the interface for formatting and parsing
058 * {@link AbstractUnit units} according to the
059 * <a href="http://unitsofmeasure.org/">Uniform Code for CommonUnits of
060 * Measure</a> (UCUM).
061 * </p>
062 *
063 * <p>
064 * For a technical/historical overview of this format please read
065 * <a href="http://www.pubmedcentral.nih.gov/articlerender.fcgi?artid=61354">
066 * CommonUnits of Measure in Clinical Information Systems</a>.
067 * </p>
068 *
069 * <p>
070 * As of revision 1.16, the BNF in the UCUM standard contains an
071 * <a href="http://unitsofmeasure.org/ticket/4">error</a>. I've attempted to
072 * work around the problem by modifying the BNF productions for &lt;Term&gt;.
073 * Once the error in the standard is corrected, it may be necessary to modify
074 * the productions in the UCUMFormatParser.jj file to conform to the standard.
075 * </p>
076 *
077 * @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a>
078 * @author <a href="mailto:units@catmedia.us">Werner Keil</a>
079 * @version 0.7.5, 30 April 2017
080 */
081public abstract class UCUMFormat extends AbstractUnitFormat {
082    /**
083     * 
084     */
085    // private static final long serialVersionUID = 8586656823290135155L;
086
087    // A helper to declare bundle names for all instances
088    private static final String BUNDLE_BASE = UCUMFormat.class.getName();
089
090    // /////////////////
091    // Class methods //
092    // /////////////////
093
094    /**
095     * Returns the instance for formatting/parsing using the given variant
096     * 
097     * @param variant
098     *            the <strong>UCUM</strong> variant to use
099     * @return a {@link UCUMFormat} instance
100     */
101    public static UCUMFormat getInstance(Variant variant) {
102        switch (variant) {
103        case CASE_INSENSITIVE:
104            return Parsing.DEFAULT_CI;
105        case CASE_SENSITIVE:
106            return Parsing.DEFAULT_CS;
107        case PRINT:
108            return Print.DEFAULT;
109        default:
110            throw new IllegalArgumentException("Unknown variant: " + variant);
111        }
112    }
113
114    /**
115     * Returns an instance for formatting and parsing using user defined symbols
116     * 
117     * @param variant
118     *            the <strong>UCUM</strong> variant to use
119     * @param symbolMap
120     *            the map of user defined symbols to use
121     * @return a {@link UCUMFormat} instance
122     */
123    public static UCUMFormat getInstance(Variant variant, SymbolMap symbolMap) {
124        switch (variant) {
125        case CASE_INSENSITIVE:
126            return new Parsing(symbolMap, false);
127        case CASE_SENSITIVE:
128            return new Parsing(symbolMap, true);
129        case PRINT:
130            return new Print(symbolMap);
131        default:
132            throw new IllegalArgumentException("Unknown variant: " + variant);
133        }
134    }
135
136    /**
137     * The symbol map used by this instance to map between {@link AbstractUnit
138     * Unit}s and <code>String</code>s.
139     */
140    final SymbolMap symbolMap;
141
142    /**
143     * Get the symbol map used by this instance to map between
144     * {@link AbstractUnit Unit}s and <code>String</code>s, etc...
145     * 
146     * @return SymbolMap the current symbol map
147     */
148    @Override
149    protected SymbolMap getSymbols() {
150        return symbolMap;
151    }
152
153    //////////////////
154    // Constructors //
155    //////////////////
156    /**
157     * Base constructor.
158     */
159    UCUMFormat(SymbolMap symbolMap) {
160        this.symbolMap = symbolMap;
161    }
162
163    // ///////////
164    // Parsing //
165    // ///////////
166    public abstract Unit<? extends Quantity<?>> parse(CharSequence csq, ParsePosition cursor) throws ParserException;
167
168    protected Unit<?> parse(CharSequence csq, int index) throws ParserException {
169        return parse(csq, new ParsePosition(index));
170    }
171
172    @Override
173    public abstract Unit<? extends Quantity<?>> parse(CharSequence csq) throws ParserException;
174
175    ////////////////
176    // Formatting //
177    ////////////////
178    @SuppressWarnings({ "rawtypes", "unchecked" })
179    public Appendable format(Unit<?> unknownUnit, Appendable appendable) throws IOException {
180        if (!(unknownUnit instanceof AbstractUnit)) {
181            throw new UnsupportedOperationException(
182                    "The UCUM format supports only known units (AbstractUnit instances)");
183        }
184        AbstractUnit unit = (AbstractUnit) unknownUnit;
185        CharSequence symbol;
186        CharSequence annotation = null;
187        if (unit instanceof AnnotatedUnit) {
188            AnnotatedUnit annotatedUnit = (AnnotatedUnit) unit;
189            unit = annotatedUnit.getActualUnit();
190            annotation = annotatedUnit.getAnnotation();
191        }
192        String mapSymbol = symbolMap.getSymbol(unit);
193        if (mapSymbol != null) {
194            symbol = mapSymbol;
195        } else if (unit instanceof TransformedUnit) {
196            final StringBuilder temp = new StringBuilder();
197            final Unit<?> parentUnit = ((TransformedUnit) unit).getParentUnit();
198            final UnitConverter converter = unit.getConverterTo(parentUnit);
199            final boolean printSeparator = !parentUnit.equals(ONE);
200
201            format(parentUnit, temp);
202            formatConverter(converter, printSeparator, temp, symbolMap);
203
204            symbol = temp;
205        } else if (unit.getBaseUnits() != null) {
206            Map<? extends AbstractUnit<?>, Integer> productUnits = unit.getBaseUnits();
207            StringBuffer app = new StringBuffer();
208            for (AbstractUnit<?> u : productUnits.keySet()) {
209                StringBuffer temp = new StringBuffer();
210                temp = (StringBuffer) format(u, temp);
211                if ((temp.indexOf(".") >= 0) || (temp.indexOf("/") >= 0)) {
212                    temp.insert(0, '(');
213                    temp.append(')');
214                }
215                int pow = productUnits.get(u);
216                int indexToAppend;
217                if (app.length() > 0) { // Not the first unit.
218
219                    if (pow >= 0) {
220
221                        if (app.indexOf("1/") >= 0) {
222                            indexToAppend = app.indexOf("1/");
223                            app.replace(indexToAppend, indexToAppend + 2, "/");
224                            // this statement make sure that (1/y).x will be
225                            // (x/y)
226
227                        } else if (app.indexOf("/") >= 0) {
228                            indexToAppend = app.indexOf("/");
229                            app.insert(indexToAppend, ".");
230                            indexToAppend++;
231                            // this statement make sure that (x/z).y will be
232                            // (x.y/z)
233
234                        } else {
235                            app.append('.');
236                            indexToAppend = app.length();
237                            // this statement make sure that (x).y will be (x.y)
238                        }
239
240                    } else {
241                        app.append('/');
242                        pow = -pow;
243
244                        indexToAppend = app.length();
245                        // this statement make sure that (x).y^-z will be
246                        // (x/y^z), where z would be added if it has a value
247                        // different than 1.
248                    }
249
250                } else { // First unit.
251
252                    if (pow < 0) {
253                        app.append("1/");
254                        pow = -pow;
255                        // this statement make sure that x^-y will be (1/x^y),
256                        // where z would be added if it has a value different
257                        // than 1.
258                    }
259
260                    indexToAppend = app.length();
261                }
262
263                app.insert(indexToAppend, temp);
264
265                if (pow != 1) {
266                    app.append(Integer.toString(pow));
267                    // this statement make sure that the power will be added if
268                    // it's different than 1.
269                }
270            }
271            symbol = app;
272        } else if (!unit.isSystemUnit() || unit.equals(SI.KILOGRAM)) {
273            final StringBuilder temp = new StringBuilder();
274            UnitConverter converter;
275            boolean printSeparator;
276            if (unit.equals(SI.KILOGRAM)) {
277                // A special case because KILOGRAM is a BaseUnit instead of
278                // a transformed unit, for compatibility with existing SI
279                // unit system.
280                format(SI.GRAM, temp);
281                converter = MetricPrefix.KILO.getConverter();
282                printSeparator = true;
283            } else {
284                Unit<?> parentUnit = unit.getSystemUnit();
285                converter = unit.getConverterTo(parentUnit);
286                if (parentUnit.equals(SI.KILOGRAM)) {
287                    // More special-case hackery to work around gram/kilogram
288                    // inconsistency
289                    parentUnit = SI.GRAM;
290                    converter = converter.concatenate(MetricPrefix.KILO.getConverter());
291                }
292                format(parentUnit, temp);
293                printSeparator = !parentUnit.equals(ONE);
294            }
295            formatConverter(converter, printSeparator, temp, symbolMap);
296            symbol = temp;
297        } else if (unit.getSymbol() != null) {
298            symbol = unit.getSymbol();
299        } else {
300            throw new IllegalArgumentException("Cannot format the given Object as UCUM units (unsupported unit "
301                    + unit.getClass().getName() + "). "
302                    + "Custom units types should override the toString() method as the default implementation uses the UCUM format.");
303        }
304        
305        appendable.append(symbol);
306        if (annotation != null && annotation.length() > 0) {
307            appendAnnotation(symbol, annotation, appendable);
308        }
309
310        return appendable;
311    }
312
313    public void label(Unit<?> unit, String label) {
314        throw new UnsupportedOperationException("label() not supported by this implementation");
315    }
316
317    public boolean isLocaleSensitive() {
318        return false;
319    }
320
321    void appendAnnotation(CharSequence symbol, CharSequence annotation, Appendable appendable) throws IOException {
322        appendable.append('{');
323        appendable.append(annotation);
324        appendable.append('}');
325    }
326
327    // static final ResourceBundle.Control getControl(final String key) {
328    // return new ResourceBundle.Control() {
329    // @Override
330    // public List<Locale> getCandidateLocales(String baseName, Locale locale) {
331    // if (baseName == null)
332    // throw new NullPointerException();
333    // if (locale.equals(new Locale(key))) {
334    // return Arrays.asList(
335    // locale,
336    // Locale.GERMANY,
337    // // no Locale.GERMAN here
338    // Locale.ROOT);
339    // } else if (locale.equals(Locale.GERMANY)) {
340    // return Arrays.asList(
341    // locale,
342    // // no Locale.GERMAN here
343    // Locale.ROOT);
344    // }
345    // return super.getCandidateLocales(baseName, locale);
346    // }
347    // };
348    // }
349
350    // /////////////////
351    // Inner classes //
352    // /////////////////
353
354    /**
355     * Variant of unit representation in the UCUM standard
356     * 
357     * @see <a href=
358     *      "http://unitsofmeasure.org/ucum.html#section-Character-Set-and-Lexical-Rules">
359     *      UCUM - Character Set and Lexical Rules</a>
360     */
361    public static enum Variant {
362        CASE_SENSITIVE, CASE_INSENSITIVE, PRINT
363    }
364
365    /**
366     * The Print format is used to output units according to the "print" column
367     * in the UCUM standard. Because "print" symbols in UCUM are not unique,
368     * this class of UCUMFormat may not be used for parsing, only for
369     * formatting.
370     */
371    private static final class Print extends UCUMFormat {
372
373        /**
374         *
375         */
376        // private static final long serialVersionUID = 2990875526976721414L;
377        private static final SymbolMap PRINT_SYMBOLS = SymbolMap.of(ResourceBundle.getBundle(BUNDLE_BASE + "_Print"));
378        private static final Print DEFAULT = new Print(PRINT_SYMBOLS);
379
380        public Print(SymbolMap symbols) {
381            super(symbols);
382        }
383
384        @Override
385        public Unit<? extends Quantity<?>> parse(CharSequence csq, ParsePosition pos) throws IllegalArgumentException {
386            throw new UnsupportedOperationException(
387                    "The print format is for pretty-printing of units only. Parsing is not supported.");
388        }
389
390        @Override
391        void appendAnnotation(CharSequence symbol, CharSequence annotation, Appendable appendable) throws IOException {
392            if (symbol != null && symbol.length() > 0) {
393                appendable.append('(');
394                appendable.append(annotation);
395                appendable.append(')');
396            } else {
397                appendable.append(annotation);
398            }
399        }
400
401        @Override
402        public Unit<? extends Quantity<?>> parse(CharSequence csq) throws IllegalArgumentException {
403            return parse(csq, new ParsePosition(0));
404
405        }
406    }
407
408    /**
409     * The Parsing format outputs formats and parses units according to the
410     * "c/s" or "c/i" column in the UCUM standard, depending on which SymbolMap
411     * is passed to its constructor.
412     */
413    private static final class Parsing extends UCUMFormat {
414        // private static final long serialVersionUID = -922531801940132715L;
415        private static final SymbolMap CASE_SENSITIVE_SYMBOLS = SymbolMap
416                .of(ResourceBundle.getBundle(BUNDLE_BASE + "_CS", new ResourceBundle.Control() {
417                    @Override
418                    public List<Locale> getCandidateLocales(String baseName, Locale locale) {
419                        if (baseName == null)
420                            throw new NullPointerException();
421                        if (locale.equals(new Locale("", "CS"))) {
422                            return Arrays.asList(locale, Locale.ROOT);
423                        }
424                        return super.getCandidateLocales(baseName, locale);
425                    }
426                }));
427        private static final SymbolMap CASE_INSENSITIVE_SYMBOLS = SymbolMap
428                .of(ResourceBundle.getBundle(BUNDLE_BASE + "_CI", new ResourceBundle.Control() {
429                    @Override
430                    public List<Locale> getCandidateLocales(String baseName, Locale locale) {
431                        if (baseName == null)
432                            throw new NullPointerException();
433                        if (locale.equals(new Locale("", "CI"))) {
434                            return Arrays.asList(locale, Locale.ROOT);
435                        } else if (locale.equals(Locale.GERMANY)) { // TODO
436                                                                    // why
437                                                                    // GERMANY?
438                            return Arrays.asList(locale,
439                                    // no Locale.GERMAN here
440                                    Locale.ROOT);
441                        }
442                        return super.getCandidateLocales(baseName, locale);
443                    }
444                }));
445        private static final Parsing DEFAULT_CS = new Parsing(CASE_SENSITIVE_SYMBOLS, true);
446        private static final Parsing DEFAULT_CI = new Parsing(CASE_INSENSITIVE_SYMBOLS, false);
447        private final boolean caseSensitive;
448
449        public Parsing(SymbolMap symbols, boolean caseSensitive) {
450            super(symbols);
451            this.caseSensitive = caseSensitive;
452        }
453
454        @Override
455        public Unit<? extends Quantity<?>> parse(CharSequence csq, ParsePosition cursor) throws ParserException {
456            // Parsing reads the whole character sequence from the parse
457            // position.
458            int start = cursor.getIndex();
459            int end = csq.length();
460            if (end <= start) {
461                return ONE;
462            }
463            String source = csq.subSequence(start, end).toString().trim();
464            if (source.length() == 0) {
465                return ONE;
466            }
467            if (!caseSensitive) {
468                source = source.toUpperCase();
469            }
470            UCUMFormatParser parser = new UCUMFormatParser(symbolMap, new ByteArrayInputStream(source.getBytes()));
471            try {
472                Unit<?> result = parser.parseUnit();
473                cursor.setIndex(end);
474                return result;
475            } catch (TokenException e) {
476                if (e.currentToken != null) {
477                    cursor.setErrorIndex(start + e.currentToken.endColumn);
478                } else {
479                    cursor.setErrorIndex(start);
480                }
481                throw new ParserException(e);
482            } catch (TokenMgrError e) {
483                cursor.setErrorIndex(start);
484                throw new IllegalArgumentException(e.getMessage());
485            }
486        }
487
488        @Override
489        public Unit<? extends Quantity<?>> parse(CharSequence csq) throws ParserException {
490            return parse(csq, new ParsePosition(0));
491        }
492    }
493}