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