View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package org.apache.commons.configuration;
    19
    20  import java.awt.Color;
    21  import java.lang.reflect.Constructor;
    22  import java.lang.reflect.InvocationTargetException;
    23  import java.math.BigDecimal;
    24  import java.math.BigInteger;
    25  import java.net.MalformedURLException;
    26  import java.net.URL;
    27  import java.text.ParseException;
    28  import java.text.SimpleDateFormat;
    29  import java.util.ArrayList;
    30  import java.util.Calendar;
    31  import java.util.Collection;
    32  import java.util.Date;
    33  import java.util.Iterator;
    34  import java.util.List;
    35  import java.util.Locale;
    36
    37  import org.apache.commons.collections.IteratorUtils;
    38  import org.apache.commons.collections.iterators.IteratorChain;
    39  import org.apache.commons.collections.iterators.SingletonIterator;
    40  import org.apache.commons.lang.BooleanUtils;
    41  import org.apache.commons.lang.StringUtils;
    42
    43  /***
    44   * A utility class to convert the configuration properties into any type.
    45   *
    46   * @author Emmanuel Bourg
    47   * @version $Revision: 442690 $, $Date: 2006-09-12 22:03:18 +0200 (Di, 12 Sep 2006) $
    48   * @since 1.1
    49   */
    50  public final class PropertyConverter
    51  {
    52      /*** Constant for the list delimiter escaping character.*/
    53      static final String LIST_ESCAPE = "//";
    54
    55      /*** Constant for the prefix of hex numbers.*/
    56      private static final String HEX_PREFIX = "0x";
    57
    58      /*** Constant for the radix of hex numbers.*/
    59      private static final int HEX_RADIX = 16;
    60
    61      /*** Constant for the argument classes of the Number constructor that takes
    62       * a String.
    63       */
    64      private static final Class[] CONSTR_ARGS = {String.class};
    65
    66      /***
    67       * Private constructor prevents instances from being created.
    68       */
    69      private PropertyConverter()
    70      {
    71          // to prevent instanciation...
    72      }
    73
    74      /***
    75       * Convert the specified object into a Boolean. Internally the
    76       * <code>org.apache.commons.lang.BooleanUtils</code> class from the
    77       * <a href="http://jakarta.apache.org/commons/lang/">Commons Lang</a>
    78       * project is used to perform this conversion. This class accepts some more
    79       * tokens for the boolean value of <b>true</b>, e.g. <code>yes</code> and
    80       * <code>on</code>. Please refer to the documentation of this class for more
    81       * details.
    82       *
    83       * @param value the value to convert
    84       * @return the converted value
    85       * @throws ConversionException thrown if the value cannot be converted to a boolean
    86       */
    87      public static Boolean toBoolean(Object value) throws ConversionException
    88      {
    89          if (value instanceof Boolean)
    90          {
    91              return (Boolean) value;
    92          }
    93          else if (value instanceof String)
    94          {
    95              Boolean b = BooleanUtils.toBooleanObject((String) value);
    96              if (b == null)
    97              {
    98                  throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
    99              }
    100             return b;
    101         }
    102         else
    103         {
    104             throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
    105         }
    106     }
    107
    108     /***
    109      * Convert the specified object into a Byte.
    110      *
    111      * @param value the value to convert
    112      * @return the converted value
    113      * @throws ConversionException thrown if the value cannot be converted to a byte
    114      */
    115     public static Byte toByte(Object value) throws ConversionException
    116     {
    117         Number n = toNumber(value, Byte.class);
    118         if (n instanceof Byte)
    119         {
    120             return (Byte) n;
    121         }
    122         else
    123         {
    124             return new Byte(n.byteValue());
    125         }
    126     }
    127
    128     /***
    129      * Convert the specified object into a Short.
    130      *
    131      * @param value the value to convert
    132      * @return the converted value
    133      * @throws ConversionException thrown if the value cannot be converted to a short
    134      */
    135     public static Short toShort(Object value) throws ConversionException
    136     {
    137         Number n = toNumber(value, Short.class);
    138         if (n instanceof Short)
    139         {
    140             return (Short) n;
    141         }
    142         else
    143         {
    144             return new Short(n.shortValue());
    145         }
    146     }
    147
    148     /***
    149      * Convert the specified object into an Integer.
    150      *
    151      * @param value the value to convert
    152      * @return the converted value
    153      * @throws ConversionException thrown if the value cannot be converted to an integer
    154      */
    155     public static Integer toInteger(Object value) throws ConversionException
    156     {
    157         Number n = toNumber(value, Integer.class);
    158         if (n instanceof Integer)
    159         {
    160             return (Integer) n;
    161         }
    162         else
    163         {
    164             return new Integer(n.intValue());
    165         }
    166     }
    167
    168     /***
    169      * Convert the specified object into a Long.
    170      *
    171      * @param value the value to convert
    172      * @return the converted value
    173      * @throws ConversionException thrown if the value cannot be converted to a Long
    174      */
    175     public static Long toLong(Object value) throws ConversionException
    176     {
    177         Number n = toNumber(value, Long.class);
    178         if (n instanceof Long)
    179         {
    180             return (Long) n;
    181         }
    182         else
    183         {
    184             return new Long(n.longValue());
    185         }
    186     }
    187
    188     /***
    189      * Convert the specified object into a Float.
    190      *
    191      * @param value the value to convert
    192      * @return the converted value
    193      * @throws ConversionException thrown if the value cannot be converted to a Float
    194      */
    195     public static Float toFloat(Object value) throws ConversionException
    196     {
    197         Number n = toNumber(value, Float.class);
    198         if (n instanceof Float)
    199         {
    200             return (Float) n;
    201         }
    202         else
    203         {
    204             return new Float(n.floatValue());
    205         }
    206     }
    207
    208     /***
    209      * Convert the specified object into a Double.
    210      *
    211      * @param value the value to convert
    212      * @return the converted value
    213      * @throws ConversionException thrown if the value cannot be converted to a Double
    214      */
    215     public static Double toDouble(Object value) throws ConversionException
    216     {
    217         Number n = toNumber(value, Double.class);
    218         if (n instanceof Double)
    219         {
    220             return (Double) n;
    221         }
    222         else
    223         {
    224             return new Double(n.doubleValue());
    225         }
    226     }
    227
    228     /***
    229      * Convert the specified object into a BigInteger.
    230      *
    231      * @param value the value to convert
    232      * @return the converted value
    233      * @throws ConversionException thrown if the value cannot be converted to a BigInteger
    234      */
    235     public static BigInteger toBigInteger(Object value) throws ConversionException
    236     {
    237         Number n = toNumber(value, BigInteger.class);
    238         if (n instanceof BigInteger)
    239         {
    240             return (BigInteger) n;
    241         }
    242         else
    243         {
    244             return BigInteger.valueOf(n.longValue());
    245         }
    246     }
    247
    248     /***
    249      * Convert the specified object into a BigDecimal.
    250      *
    251      * @param value the value to convert
    252      * @return the converted value
    253      * @throws ConversionException thrown if the value cannot be converted to a BigDecimal
    254      */
    255     public static BigDecimal toBigDecimal(Object value) throws ConversionException
    256     {
    257         Number n = toNumber(value, BigDecimal.class);
    258         if (n instanceof BigDecimal)
    259         {
    260             return (BigDecimal) n;
    261         }
    262         else
    263         {
    264             return new BigDecimal(n.doubleValue());
    265         }
    266     }
    267
    268     /***
    269      * Tries to convert the specified object into a number object. This method
    270      * is used by the conversion methods for number types. Note that the return
    271      * value is not in always of the specified target class, but only if a new
    272      * object has to be created.
    273      *
    274      * @param value the value to be converted (must not be <b>null</b>)
    275      * @param targetClass the target class of the conversion (must be derived
    276      * from <code>java.lang.Number</code>)
    277      * @return the converted number
    278      * @throws ConversionException if the object cannot be converted
    279      */
    280     static Number toNumber(Object value, Class targetClass)
    281             throws ConversionException
    282     {
    283         if (value instanceof Number)
    284         {
    285             return (Number) value;
    286         }
    287         else
    288         {
    289             String str = value.toString();
    290             if (str.startsWith(HEX_PREFIX))
    291             {
    292                 try
    293                 {
    294                     return new BigInteger(str.substring(HEX_PREFIX.length()),
    295                             HEX_RADIX);
    296                 }
    297                 catch (NumberFormatException nex)
    298                 {
    299                     throw new ConversionException("Could not convert " + str
    300                             + " to " + targetClass.getName()
    301                             + "! Invalid hex number.", nex);
    302                 }
    303             }
    304
    305             try
    306             {
    307                 Constructor constr = targetClass.getConstructor(CONSTR_ARGS);
    308                 return (Number) constr.newInstance(new Object[]{str});
    309             }
    310             catch (InvocationTargetException itex)
    311             {
    312                 throw new ConversionException("Could not convert " + str
    313                         + " to " + targetClass.getName(), itex
    314                         .getTargetException());
    315             }
    316             catch (Exception ex)
    317             {
    318                 // Treat all possible exceptions the same way
    319                 throw new ConversionException(
    320                         "Conversion error when trying to convert " + str
    321                                 + " to " + targetClass.getName(), ex);
    322             }
    323         }
    324     }
    325
    326     /***
    327      * Convert the specified object into an URL.
    328      *
    329      * @param value the value to convert
    330      * @return the converted value
    331      * @throws ConversionException thrown if the value cannot be converted to an URL
    332      */
    333     public static URL toURL(Object value) throws ConversionException
    334     {
    335         if (value instanceof URL)
    336         {
    337             return (URL) value;
    338         }
    339         else if (value instanceof String)
    340         {
    341             try
    342             {
    343                 return new URL((String) value);
    344             }
    345             catch (MalformedURLException e)
    346             {
    347                 throw new ConversionException("The value " + value + " can't be converted to an URL", e);
    348             }
    349         }
    350         else
    351         {
    352             throw new ConversionException("The value " + value + " can't be converted to an URL");
    353         }
    354     }
    355
    356     /***
    357      * Convert the specified object into a Locale.
    358      *
    359      * @param value the value to convert
    360      * @return the converted value
    361      * @throws ConversionException thrown if the value cannot be converted to a Locale
    362      */
    363     public static Locale toLocale(Object value) throws ConversionException
    364     {
    365         if (value instanceof Locale)
    366         {
    367             return (Locale) value;
    368         }
    369         else if (value instanceof String)
    370         {
    371             List elements = split((String) value, '_');
    372             int size = elements.size();
    373
    374             if (size >= 1 && (((String) elements.get(0)).length() == 2 || ((String) elements.get(0)).length() == 0))
    375             {
    376                 String language = (String) elements.get(0);
    377                 String country = (String) ((size >= 2) ? elements.get(1) : "");
    378                 String variant = (String) ((size >= 3) ? elements.get(2) : "");
    379
    380                 return new Locale(language, country, variant);
    381             }
    382             else
    383             {
    384                 throw new ConversionException("The value " + value + " can't be converted to a Locale");
    385             }
    386         }
    387         else
    388         {
    389             throw new ConversionException("The value " + value + " can't be converted to a Locale");
    390         }
    391     }
    392
    393     /***
    394      * Split a string on the specified delimiter. To be removed when
    395      * commons-lang has a better replacement available (Tokenizer?).
    396      *
    397      * todo: replace with a commons-lang equivalent
    398      *
    399      * @param s          the string to split
    400      * @param delimiter  the delimiter
    401      * @return a list with the single tokens
    402      */
    403     public static List split(String s, char delimiter)
    404     {
    405         if (s == null)
    406         {
    407             return new ArrayList();
    408         }
    409
    410         List list = new ArrayList();
    411
    412         StringBuffer token = new StringBuffer();
    413         int begin = 0;
    414         int end = 0;
    415         while (begin <= s.length())
    416         {
    417             // find the next delimiter
    418             int index = s.indexOf(delimiter, end);
    419
    420             // move the end index at the end of the string if the delimiter is not found
    421             end = (index != -1) ? index : s.length();
    422
    423             // extract the chunk
    424             String chunk = s.substring(begin , end);
    425
    426             if (chunk.endsWith(LIST_ESCAPE) && end != s.length())
    427             {
    428                 token.append(chunk.substring(0, chunk.length() - 1));
    429                 token.append(delimiter);
    430             }
    431             else
    432             {
    433                 // append the chunk to the token
    434                 token.append(chunk);
    435
    436                 // add the token to the list
    437                 list.add(token.toString().trim());
    438
    439                 // reset the token
    440                 token = new StringBuffer();
    441             }
    442
    443             // move to the next chunk
    444             end = end + 1;
    445             begin = end;
    446         }
    447
    448         return list;
    449     }
    450
    451     /***
    452      * Escapes the delimiters that might be contained in the given string. This
    453      * method ensures that list delimiter characters that are part of a
    454      * property's value are correctly escaped when a configuration is saved to a
    455      * file. Otherwise when loaded again the property will be treated as a list
    456      * property.
    457      *
    458      * @param s the string with the value
    459      * @param delimiter the list delimiter to use
    460      * @return the correctly esaped string
    461      */
    462     public static String escapeDelimiters(String s, char delimiter)
    463     {
    464         return StringUtils.replace(s, String.valueOf(delimiter), LIST_ESCAPE
    465                 + delimiter);
    466     }
    467
    468     /***
    469      * Convert the specified object into a Color. If the value is a String,
    470      * the format allowed is (#)?[0-9A-F]{6}([0-9A-F]{2})?. Examples:
    471      * <ul>
    472      *   <li>FF0000 (red)</li>
    473      *   <li>0000FFA0 (semi transparent blue)</li>
    474      *   <li>#CCCCCC (gray)</li>
    475      *   <li>#00FF00A0 (semi transparent green)</li>
    476      * </ul>
    477      *
    478      * @param value the value to convert
    479      * @return the converted value
    480      * @throws ConversionException thrown if the value cannot be converted to a Color
    481      */
    482     public static Color toColor(Object value) throws ConversionException
    483     {
    484         if (value instanceof Color)
    485         {
    486             return (Color) value;
    487         }
    488         else if (value instanceof String && !StringUtils.isBlank((String) value))
    489         {
    490             String color = ((String) value).trim();
    491
    492             int[] components = new int[3];
    493
    494             // check the size of the string
    495             int minlength = components.length * 2;
    496             if (color.length() < minlength)
    497             {
    498                 throw new ConversionException("The value " + value + " can't be converted to a Color");
    499             }
    500
    501             // remove the leading #
    502             if (color.startsWith("#"))
    503             {
    504                 color = color.substring(1);
    505             }
    506
    507             try
    508             {
    509                 // parse the components
    510                 for (int i = 0; i < components.length; i++)
    511                 {
    512                     components[i] = Integer.parseInt(color.substring(2 * i, 2 * i + 2), HEX_RADIX);
    513                 }
    514
    515                 // parse the transparency
    516                 int alpha;
    517                 if (color.length() >= minlength + 2)
    518                 {
    519                     alpha = Integer.parseInt(color.substring(minlength, minlength + 2), HEX_RADIX);
    520                 }
    521                 else
    522                 {
    523                     alpha = Color.black.getAlpha();
    524                 }
    525
    526                 return new Color(components[0], components[1], components[2], alpha);
    527             }
    528             catch (Exception e)
    529             {
    530                 throw new ConversionException("The value " + value + " can't be converted to a Color", e);
    531             }
    532         }
    533         else
    534         {
    535             throw new ConversionException("The value " + value + " can't be converted to a Color");
    536         }
    537     }
    538
    539     /***
    540      * Convert the specified object into a Date.
    541      *
    542      * @param value  the value to convert
    543      * @param format the DateFormat pattern to parse String values
    544      * @return the converted value
    545      * @throws ConversionException thrown if the value cannot be converted to a Calendar
    546      */
    547     public static Date toDate(Object value, String format) throws ConversionException
    548     {
    549         if (value instanceof Date)
    550         {
    551             return (Date) value;
    552         }
    553         else if (value instanceof Calendar)
    554         {
    555             return ((Calendar) value).getTime();
    556         }
    557         else if (value instanceof String)
    558         {
    559             try
    560             {
    561                 return new SimpleDateFormat(format).parse((String) value);
    562             }
    563             catch (ParseException e)
    564             {
    565                 throw new ConversionException("The value " + value + " can't be converted to a Date", e);
    566             }
    567         }
    568         else
    569         {
    570             throw new ConversionException("The value " + value + " can't be converted to a Date");
    571         }
    572     }
    573
    574     /***
    575      * Convert the specified object into a Calendar.
    576      *
    577      * @param value  the value to convert
    578      * @param format the DateFormat pattern to parse String values
    579      * @return the converted value
    580      * @throws ConversionException thrown if the value cannot be converted to a Calendar
    581      */
    582     public static Calendar toCalendar(Object value, String format) throws ConversionException
    583     {
    584         if (value instanceof Calendar)
    585         {
    586             return (Calendar) value;
    587         }
    588         else if (value instanceof Date)
    589         {
    590             Calendar calendar = Calendar.getInstance();
    591             calendar.setTime((Date) value);
    592             return calendar;
    593         }
    594         else if (value instanceof String)
    595         {
    596             try
    597             {
    598                 Calendar calendar = Calendar.getInstance();
    599                 calendar.setTime(new SimpleDateFormat(format).parse((String) value));
    600                 return calendar;
    601             }
    602             catch (ParseException e)
    603             {
    604                 throw new ConversionException("The value " + value + " can't be converted to a Calendar", e);
    605             }
    606         }
    607         else
    608         {
    609             throw new ConversionException("The value " + value + " can't be converted to a Calendar");
    610         }
    611     }
    612
    613     /***
    614      * Return an iterator over the simple values of a composite value. The value
    615      * specified is handled depending on its type:
    616      * <ul>
    617      *   <li>Strings are checked for delimiter characters and splitted if necessary.</li>
    618      *   <li>For collections the single elements are checked.</li>
    619      *   <li>Arrays are treated like collections.</li>
    620      *   <li>All other types are directly inserted.</li>
    621      *   <li>Recursive combinations are supported, e.g. a collection containing array that contain strings.</li>
    622      * </ul>
    623      *
    624      * @param value     the value to "split"
    625      * @param delimiter the delimiter for String values
    626      * @return an iterator for accessing the single values
    627      */
    628     public static Iterator toIterator(Object value, char delimiter)
    629     {
    630         if (value == null)
    631         {
    632             return IteratorUtils.emptyIterator();
    633         }
    634         if (value instanceof String)
    635         {
    636             String s = (String) value;
    637             if (s.indexOf(delimiter) > 0)
    638             {
    639                 return split((String) value, delimiter).iterator();
    640             }
    641             else
    642             {
    643                 return new SingletonIterator(value);
    644             }
    645         }
    646         else if (value instanceof Collection)
    647         {
    648             return toIterator(((Collection) value).iterator(), delimiter);
    649         }
    650         else if (value.getClass().isArray())
    651         {
    652             return toIterator(IteratorUtils.arrayIterator(value), delimiter);
    653         }
    654         else if (value instanceof Iterator)
    655         {
    656             Iterator iterator = (Iterator) value;
    657             IteratorChain chain = new IteratorChain();
    658             while (iterator.hasNext())
    659             {
    660                 chain.addIterator(toIterator(iterator.next(), delimiter));
    661             }
    662             return chain;
    663         }
    664         else
    665         {
    666             return new SingletonIterator(value);
    667         }
    668     }
    669
    670     /***
    671      * Performs interpolation of the specified value. This method checks if the
    672      * given value contains variables of the form <code>${...}</code>. If
    673      * this is the case, all occurrances will be substituted by their current
    674      * values.
    675      *
    676      * @param value the value to be interpolated
    677      * @param config the current configuration object
    678      * @return the interpolated value
    679      */
    680     public static Object interpolate(Object value, AbstractConfiguration config)
    681     {
    682         if (value instanceof String)
    683         {
    684             return interpolateHelper((String) value, null, config);
    685         }
    686         else
    687         {
    688             return value;
    689         }
    690     }
    691
    692     /***
    693      * Recursive handler for multple levels of interpolation. This will be
    694      * replaced when Commons Lang provides an interpolation feature. When called
    695      * the first time, priorVariables should be null.
    696      *
    697      * @param base string with the ${key} variables
    698      * @param priorVariables serves two purposes: to allow checking for loops,
    699      * and creating a meaningful exception message should a loop occur. It's
    700      * 0'th element will be set to the value of base from the first call. All
    701      * subsequent interpolated variables are added afterward.
    702      * @param config the current configuration
    703      * @return the string with the interpolation taken care of
    704      */
    705     private static String interpolateHelper(String base, List priorVariables,
    706             AbstractConfiguration config)
    707     {
    708         if (base == null)
    709         {
    710             return null;
    711         }
    712
    713         // on the first call initialize priorVariables
    714         // and add base as the first element
    715         if (priorVariables == null)
    716         {
    717             priorVariables = new ArrayList();
    718             priorVariables.add(base);
    719         }
    720
    721         int begin = -1;
    722         int end = -1;
    723         int prec = 0 - AbstractConfiguration.END_TOKEN.length();
    724         StringBuffer result = new StringBuffer();
    725
    726         // FIXME: we should probably allow the escaping of the start token
    727         while (((begin = base.indexOf(AbstractConfiguration.START_TOKEN, prec
    728                 + AbstractConfiguration.END_TOKEN.length())) > -1)
    729                 && ((end = base.indexOf(AbstractConfiguration.END_TOKEN, begin)) > -1))
    730         {
    731             result.append(base.substring(prec
    732                     + AbstractConfiguration.END_TOKEN.length(), begin));
    733             String variable = base.substring(begin
    734                     + AbstractConfiguration.START_TOKEN.length(), end);
    735
    736             // if we've got a loop, create a useful exception message and throw
    737             if (priorVariables.contains(variable))
    738             {
    739                 String initialBase = priorVariables.remove(0).toString();
    740                 priorVariables.add(variable);
    741                 StringBuffer priorVariableSb = new StringBuffer();
    742
    743                 // create a nice trace of interpolated variables like so:
    744                 // var1->var2->var3
    745                 for (Iterator it = priorVariables.iterator(); it.hasNext();)
    746                 {
    747                     priorVariableSb.append(it.next());
    748                     if (it.hasNext())
    749                     {
    750                         priorVariableSb.append("->");
    751                     }
    752                 }
    753
    754                 throw new IllegalStateException(
    755                         "infinite loop in property interpolation of "
    756                                 + initialBase + ": "
    757                                 + priorVariableSb.toString());
    758             }
    759             // otherwise, add this variable to the interpolation list.
    760             else
    761             {
    762                 priorVariables.add(variable);
    763             }
    764
    765             Object value = config.resolveContainerStore(variable);
    766             if (value != null)
    767             {
    768                 result.append(interpolateHelper(value.toString(),
    769                         priorVariables, config));
    770
    771                 // pop the interpolated variable off the stack
    772                 // this maintains priorVariables correctness for
    773                 // properties with multiple interpolations, e.g.
    774                 // prop.name=${some.other.prop1}/blahblah/${some.other.prop2}
    775                 priorVariables.remove(priorVariables.size() - 1);
    776             }
    777             else
    778             {
    779                 // variable not defined - so put it back in the value
    780                 result.append(AbstractConfiguration.START_TOKEN);
    781                 result.append(variable);
    782                 result.append(AbstractConfiguration.END_TOKEN);
    783             }
    784
    785             prec = end;
    786         }
    787         result.append(base.substring(prec
    788                 + AbstractConfiguration.END_TOKEN.length(), base.length()));
    789         return result.toString();
    790     }
    791 }