1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
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
418 int index = s.indexOf(delimiter, end);
419
420
421 end = (index != -1) ? index : s.length();
422
423
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
434 token.append(chunk);
435
436
437 list.add(token.toString().trim());
438
439
440 token = new StringBuffer();
441 }
442
443
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
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
502 if (color.startsWith("#"))
503 {
504 color = color.substring(1);
505 }
506
507 try
508 {
509
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
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
714
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
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
737 if (priorVariables.contains(variable))
738 {
739 String initialBase = priorVariables.remove(0).toString();
740 priorVariables.add(variable);
741 StringBuffer priorVariableSb = new StringBuffer();
742
743
744
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
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
772
773
774
775 priorVariables.remove(priorVariables.size() - 1);
776 }
777 else
778 {
779
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 }