001/*
002 * Units of Measurement Reference Implementation
003 * Copyright (c) 2005-2020, Units of Measurement project.
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-385, Indriya nor the names of their contributors may be used to endorse or promote products
017 *    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 tech.units.indriya.format;
031
032import javax.measure.Prefix;
033import javax.measure.Unit;
034import javax.measure.UnitConverter;
035
036import tech.units.indriya.AbstractUnit;
037import tech.units.indriya.function.AbstractConverter;
038import tech.units.indriya.function.MultiplyConverter;
039
040import java.lang.reflect.Field;
041import java.util.Collections;
042import java.util.Comparator;
043import java.util.Enumeration;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047import java.util.ResourceBundle;
048import java.util.TreeMap;
049import java.util.logging.Level;
050import java.util.logging.Logger;
051import java.util.stream.Collectors;
052
053/**
054 * <p>
055 * This class provides a set of mappings between {@link AbstractUnit units} and symbols (both ways), between {@link MetricPrefix prefixes} and symbols
056 * (both ways), and from {@link AbstractConverter unit converters} to {@link MetricPrefix prefixes} (one way). No attempt is made to verify the
057 * uniqueness of the mappings.
058 * </p>
059 *
060 * <p>
061 * Mappings are read from a <code>ResourceBundle</code>, the keys of which should consist of a fully-qualified class name, followed by a dot ('.'),
062 * and then the name of a static field belonging to that class, followed optionally by another dot and a number. If the trailing dot and number are
063 * not present, the value associated with the key is treated as a {@link SymbolMap#label(AbstractUnit, String) label}, otherwise if the trailing dot
064 * and number are present, the value is treated as an {@link SymbolMap#alias(AbstractUnit,String) alias}. Aliases map from String to Unit only,
065 * whereas labels map in both directions. A given unit may have any number of aliases, but may have only one label.
066 * </p>
067 *
068 * @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a>
069 * @author <a href="mailto:werner@units.tech">Werner Keil</a>
070 * @version 1.7, February 25, 2017
071 */
072@SuppressWarnings("rawtypes")
073public final class SymbolMap {
074  private static final Logger logger = Logger.getLogger(SymbolMap.class.getName());
075
076  private final Map<String, Unit<?>> symbolToUnit;
077  private final Map<Unit<?>, String> unitToSymbol;
078  private final Map<String, Object> symbolToPrefix;
079  private final Map<Object, String> prefixToSymbol;
080  private final Map<UnitConverter, Prefix> converterToPrefix;
081
082  /**
083   * Creates an empty mapping.
084   */
085  private SymbolMap() {
086    symbolToUnit = new TreeMap<>();
087    unitToSymbol = new HashMap<>();
088    symbolToPrefix = new TreeMap<>();
089    prefixToSymbol = new HashMap<>();
090    converterToPrefix = new HashMap<>();
091  }
092
093  /**
094   * Creates a symbol map from the specified resource bundle,
095   *
096   * @param rb
097   *          the resource bundle.
098   */
099  private SymbolMap(ResourceBundle rb) {
100    this();
101    for (Enumeration<String> i = rb.getKeys(); i.hasMoreElements();) {
102      String fqn = i.nextElement();
103      String symbol = rb.getString(fqn);
104      boolean isAlias = false;
105      int lastDot = fqn.lastIndexOf('.');
106      String className = fqn.substring(0, lastDot);
107      String fieldName = fqn.substring(lastDot + 1, fqn.length());
108      if (Character.isDigit(fieldName.charAt(0))) {
109        isAlias = true;
110        fqn = className;
111        lastDot = fqn.lastIndexOf('.');
112        className = fqn.substring(0, lastDot);
113        fieldName = fqn.substring(lastDot + 1, fqn.length());
114      }
115      try {
116        Class<?> c = Class.forName(className);
117        Field field = c.getField(fieldName);
118        Object value = field.get(null);
119        if (value instanceof Unit<?>) {
120          if (isAlias) {
121            alias((Unit) value, symbol);
122          } else {
123            label((AbstractUnit<?>) value, symbol);
124          }
125        } else if (value instanceof Prefix) {
126          label((Prefix) value, symbol);
127        } else {
128          throw new ClassCastException("unable to cast " + value + " to Unit or Prefix");
129        }
130      } catch (Exception error) {
131        logger.log(Level.SEVERE, "Error", error);
132      }
133    }
134  }
135
136  /**
137   * Creates a symbol map from the specified resource bundle,
138   *
139   * @param rb
140   *          the resource bundle.
141   */
142  public static SymbolMap of(ResourceBundle rb) {
143    return new SymbolMap(rb);
144  }
145
146  /**
147   * Attaches a label to the specified unit. For example:<br>
148   * <code> symbolMap.label(DAY.multiply(365), "year"); symbolMap.label(US.FOOT, "ft");
149   * </code>
150   *
151   * @param unit
152   *          the unit to label.
153   * @param symbol
154   *          the new symbol for the unit.
155   */
156  public void label(Unit<?> unit, String symbol) {
157    symbolToUnit.put(symbol, unit);
158    unitToSymbol.put(unit, symbol);
159  }
160
161  /**
162   * Attaches an alias to the specified unit. Multiple aliases may be attached to the same unit. Aliases are used during parsing to recognize
163   * different variants of the same unit.<code> symbolMap.alias(US.FOOT, "foot"); symbolMap.alias(US.FOOT, "feet");
164   * symbolMap.alias(Units.METER, "meter"); symbolMap.alias(Units.METER, "metre"); </code>
165   *
166   * @param unit
167   *          the unit to label.
168   * @param symbol
169   *          the new symbol for the unit.
170   */
171  public void alias(Unit<?> unit, String symbol) {
172    symbolToUnit.put(symbol, unit);
173  }
174
175  /**
176   * Attaches a label to the specified prefix. For example:<br>
177   * <code> symbolMap.label(MetricPrefix.GIGA, "G"); symbolMap.label(MetricPrefix.MICRO, "ยต");
178   * </code>
179   * 
180   * TODO should be able to do this with a generic Prefix
181   */
182  public void label(Prefix prefix, String symbol) {
183    symbolToPrefix.put(symbol, prefix);
184    prefixToSymbol.put(prefix, symbol);
185    converterToPrefix.put(MultiplyConverter.ofPrefix(prefix), prefix);
186  }
187
188  /**
189   * Returns the unit for the specified symbol.
190   *
191   * @param symbol
192   *          the symbol.
193   * @return the corresponding unit or <code>null</code> if none.
194   */
195  public Unit<?> getUnit(String symbol) {
196    return symbolToUnit.get(symbol);
197  }
198
199  /**
200   * Returns the symbol (label) for the specified unit.
201   *
202   * @param unit
203   *          the corresponding symbol.
204   * @return the corresponding symbol or <code>null</code> if none.
205   */
206  public String getSymbol(Unit<?> unit) {
207    return unitToSymbol.get(unit);
208  }
209
210  /**
211   * Returns the prefix (if any) for the specified symbol.
212   *
213   * @param symbol
214   *          the unit symbol.
215   * @return the corresponding prefix or <code>null</code> if none.
216   */
217  public Prefix getPrefix(String symbol) {
218        final List<String> list = symbolToPrefix.keySet().stream().collect(Collectors.toList());
219        final Comparator<String> comparator = Comparator.comparing(String::length);
220        Collections.sort(list, comparator.reversed());
221
222        for (String key : list) {
223            if (symbol.startsWith(key)) {
224                return (Prefix) symbolToPrefix.get(key);
225            }
226        }
227        return null;
228    }
229
230  /**
231   * Returns the prefix for the specified converter.
232   *
233   * @param converter
234   *          the unit converter.
235   * @return the corresponding prefix or <code>null</code> if none.
236   */
237  public Prefix getPrefix(UnitConverter converter) {
238    return converterToPrefix.get(converter);
239  }
240
241  /**
242   * Returns the symbol for the specified prefix.
243   *
244   * @param prefix
245   *          the prefix.
246   * @return the corresponding symbol or <code>null</code> if none.
247   */
248  public String getSymbol(Prefix prefix) {
249    return prefixToSymbol.get(prefix);
250  }
251
252  @Override
253  public String toString() {
254    StringBuilder sb = new StringBuilder();
255    sb.append("tech.units.indriya.format.SymbolMap: [");
256    sb.append("symbolToUnit: ").append(symbolToUnit).append(',');
257    sb.append("unitToSymbol: ").append(unitToSymbol).append(',');
258    sb.append("symbolToPrefix: ").append(symbolToPrefix).append(',');
259    sb.append("prefixToSymbol: ").append(prefixToSymbol).append(',');
260    sb.append("converterToPrefix: ").append(converterToPrefix).append(',');
261    sb.append("converterToPrefix: ").append(converterToPrefix);
262    sb.append(" ]");
263    return sb.toString();
264  }
265
266}