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 }