001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.configuration;
019
020import java.awt.Color;
021import java.lang.reflect.Array;
022import java.lang.reflect.Constructor;
023import java.lang.reflect.InvocationTargetException;
024import java.math.BigDecimal;
025import java.math.BigInteger;
026import java.net.InetAddress;
027import java.net.MalformedURLException;
028import java.net.URL;
029import java.net.UnknownHostException;
030import java.nio.file.Path;
031import java.text.ParseException;
032import java.text.SimpleDateFormat;
033import java.util.ArrayList;
034import java.util.Calendar;
035import java.util.Collection;
036import java.util.Collections;
037import java.util.Date;
038import java.util.IdentityHashMap;
039import java.util.Iterator;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Locale;
043import java.util.Set;
044
045import org.apache.commons.lang.BooleanUtils;
046import org.apache.commons.lang.StringUtils;
047
048/**
049 * A utility class to convert the configuration properties into any type.
050 *
051 * @author Emmanuel Bourg
052 * @version $Id: PropertyConverter.java 1534376 2013-10-21 21:14:18Z henning $
053 * @since 1.1
054 */
055public final class PropertyConverter
056{
057    /** Constant for the list delimiter as char.*/
058    static final char LIST_ESC_CHAR = '\\';
059
060    /** Constant for the list delimiter escaping character as string.*/
061    static final String LIST_ESCAPE = String.valueOf(LIST_ESC_CHAR);
062
063    /** Constant for the prefix of hex numbers.*/
064    private static final String HEX_PREFIX = "0x";
065
066    /** Constant for the radix of hex numbers.*/
067    private static final int HEX_RADIX = 16;
068
069    /** Constant for the prefix of binary numbers.*/
070    private static final String BIN_PREFIX = "0b";
071
072    /** Constant for the radix of binary numbers.*/
073    private static final int BIN_RADIX = 2;
074
075    /** Constant for the argument classes of the Number constructor that takes a String. */
076    private static final Class<?>[] CONSTR_ARGS = {String.class};
077
078    /** The fully qualified name of {@link javax.mail.internet.InternetAddress} */
079    private static final String INTERNET_ADDRESS_CLASSNAME = "javax.mail.internet.InternetAddress";
080
081    /**
082     * Private constructor prevents instances from being created.
083     */
084    private PropertyConverter()
085    {
086        // to prevent instantiation...
087    }
088
089    /**
090     * Converts the specified value to the target class. If the class is a
091     * primitive type (Integer.TYPE, Boolean.TYPE, etc) the value returned
092     * will use the wrapper type (Integer.class, Boolean.class, etc).
093     *
094     * @param cls   the target class of the converted value
095     * @param value the value to convert
096     * @param params optional parameters used for the conversion
097     * @return the converted value
098     * @throws ConversionException if the value is not compatible with the requested type
099     *
100     * @since 1.5
101     */
102    static Object to(Class<?> cls, Object value, Object[] params) throws ConversionException
103    {
104        if (cls.isInstance(value))
105        {
106            return value; // no conversion needed
107        }
108
109        if (Boolean.class.equals(cls) || Boolean.TYPE.equals(cls))
110        {
111            return toBoolean(value);
112        }
113        else if (Character.class.equals(cls) || Character.TYPE.equals(cls))
114        {
115            return toCharacter(value);
116        }
117        else if (Number.class.isAssignableFrom(cls) || cls.isPrimitive())
118        {
119            if (Integer.class.equals(cls) || Integer.TYPE.equals(cls))
120            {
121                return toInteger(value);
122            }
123            else if (Long.class.equals(cls) || Long.TYPE.equals(cls))
124            {
125                return toLong(value);
126            }
127            else if (Byte.class.equals(cls) || Byte.TYPE.equals(cls))
128            {
129                return toByte(value);
130            }
131            else if (Short.class.equals(cls) || Short.TYPE.equals(cls))
132            {
133                return toShort(value);
134            }
135            else if (Float.class.equals(cls) || Float.TYPE.equals(cls))
136            {
137                return toFloat(value);
138            }
139            else if (Double.class.equals(cls) || Double.TYPE.equals(cls))
140            {
141                return toDouble(value);
142            }
143            else if (BigInteger.class.equals(cls))
144            {
145                return toBigInteger(value);
146            }
147            else if (BigDecimal.class.equals(cls))
148            {
149                return toBigDecimal(value);
150            }
151        }
152        else if (Date.class.equals(cls))
153        {
154            return toDate(value, (String) params[0]);
155        }
156        else if (Calendar.class.equals(cls))
157        {
158            return toCalendar(value, (String) params[0]);
159        }
160        else if (URL.class.equals(cls))
161        {
162            return toURL(value);
163        }
164        else if (Locale.class.equals(cls))
165        {
166            return toLocale(value);
167        }
168        else if (isEnum(cls))
169        {
170            return convertToEnum(cls, value);
171        }
172        else if (Color.class.equals(cls))
173        {
174            return toColor(value);
175        }
176        else if (cls.getName().equals(INTERNET_ADDRESS_CLASSNAME))
177        {
178            return toInternetAddress(value);
179        }
180        else if (InetAddress.class.isAssignableFrom(cls))
181        {
182            return toInetAddress(value);
183        }
184
185        throw new ConversionException("The value '" + value + "' (" + value.getClass() + ")"
186                + " can't be converted to a " + cls.getName() + " object");
187    }
188
189    /**
190     * Convert the specified object into a Boolean. Internally the
191     * {@code org.apache.commons.lang.BooleanUtils} class from the
192     * <a href="http://commons.apache.org/lang/">Commons Lang</a>
193     * project is used to perform this conversion. This class accepts some more
194     * tokens for the boolean value of <b>true</b>, e.g. {@code yes} and
195     * {@code on}. Please refer to the documentation of this class for more
196     * details.
197     *
198     * @param value the value to convert
199     * @return the converted value
200     * @throws ConversionException thrown if the value cannot be converted to a boolean
201     */
202    public static Boolean toBoolean(Object value) throws ConversionException
203    {
204        if (value instanceof Boolean)
205        {
206            return (Boolean) value;
207        }
208        else if (value instanceof String)
209        {
210            Boolean b = BooleanUtils.toBooleanObject((String) value);
211            if (b == null)
212            {
213                throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
214            }
215            return b;
216        }
217        else
218        {
219            throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
220        }
221    }
222
223    /**
224     * Converts the specified value object to a {@code Character}. This method
225     * converts the passed in object to a string. If the string has exactly one
226     * character, this character is returned as result. Otherwise, conversion
227     * fails.
228     *
229     * @param value the value to be converted
230     * @return the resulting {@code Character} object
231     * @throws ConversionException if the conversion is not possible
232     */
233    public static Character toCharacter(Object value) throws ConversionException
234    {
235        String strValue = String.valueOf(value);
236        if (strValue.length() == 1)
237        {
238            return Character.valueOf(strValue.charAt(0));
239        }
240        else
241        {
242            throw new ConversionException(
243                    String.format(
244                            "The value '%s' cannot be converted to a Character object!",
245                            strValue));
246        }
247    }
248
249    /**
250     * Convert the specified object into a Byte.
251     *
252     * @param value the value to convert
253     * @return the converted value
254     * @throws ConversionException thrown if the value cannot be converted to a byte
255     */
256    public static Byte toByte(Object value) throws ConversionException
257    {
258        Number n = toNumber(value, Byte.class);
259        if (n instanceof Byte)
260        {
261            return (Byte) n;
262        }
263        else
264        {
265            return new Byte(n.byteValue());
266        }
267    }
268
269    /**
270     * Convert the specified object into a Short.
271     *
272     * @param value the value to convert
273     * @return the converted value
274     * @throws ConversionException thrown if the value cannot be converted to a short
275     */
276    public static Short toShort(Object value) throws ConversionException
277    {
278        Number n = toNumber(value, Short.class);
279        if (n instanceof Short)
280        {
281            return (Short) n;
282        }
283        else
284        {
285            return new Short(n.shortValue());
286        }
287    }
288
289    /**
290     * Convert the specified object into an Integer.
291     *
292     * @param value the value to convert
293     * @return the converted value
294     * @throws ConversionException thrown if the value cannot be converted to an integer
295     */
296    public static Integer toInteger(Object value) throws ConversionException
297    {
298        Number n = toNumber(value, Integer.class);
299        if (n instanceof Integer)
300        {
301            return (Integer) n;
302        }
303        else
304        {
305            return new Integer(n.intValue());
306        }
307    }
308
309    /**
310     * Convert the specified object into a Long.
311     *
312     * @param value the value to convert
313     * @return the converted value
314     * @throws ConversionException thrown if the value cannot be converted to a Long
315     */
316    public static Long toLong(Object value) throws ConversionException
317    {
318        Number n = toNumber(value, Long.class);
319        if (n instanceof Long)
320        {
321            return (Long) n;
322        }
323        else
324        {
325            return new Long(n.longValue());
326        }
327    }
328
329    /**
330     * Convert the specified object into a Float.
331     *
332     * @param value the value to convert
333     * @return the converted value
334     * @throws ConversionException thrown if the value cannot be converted to a Float
335     */
336    public static Float toFloat(Object value) throws ConversionException
337    {
338        Number n = toNumber(value, Float.class);
339        if (n instanceof Float)
340        {
341            return (Float) n;
342        }
343        else
344        {
345            return new Float(n.floatValue());
346        }
347    }
348
349    /**
350     * Convert the specified object into a Double.
351     *
352     * @param value the value to convert
353     * @return the converted value
354     * @throws ConversionException thrown if the value cannot be converted to a Double
355     */
356    public static Double toDouble(Object value) throws ConversionException
357    {
358        Number n = toNumber(value, Double.class);
359        if (n instanceof Double)
360        {
361            return (Double) n;
362        }
363        else
364        {
365            return new Double(n.doubleValue());
366        }
367    }
368
369    /**
370     * Convert the specified object into a BigInteger.
371     *
372     * @param value the value to convert
373     * @return the converted value
374     * @throws ConversionException thrown if the value cannot be converted to a BigInteger
375     */
376    public static BigInteger toBigInteger(Object value) throws ConversionException
377    {
378        Number n = toNumber(value, BigInteger.class);
379        if (n instanceof BigInteger)
380        {
381            return (BigInteger) n;
382        }
383        else
384        {
385            return BigInteger.valueOf(n.longValue());
386        }
387    }
388
389    /**
390     * Convert the specified object into a BigDecimal.
391     *
392     * @param value the value to convert
393     * @return the converted value
394     * @throws ConversionException thrown if the value cannot be converted to a BigDecimal
395     */
396    public static BigDecimal toBigDecimal(Object value) throws ConversionException
397    {
398        Number n = toNumber(value, BigDecimal.class);
399        if (n instanceof BigDecimal)
400        {
401            return (BigDecimal) n;
402        }
403        else
404        {
405            return new BigDecimal(n.doubleValue());
406        }
407    }
408
409    /**
410     * Tries to convert the specified object into a number object. This method
411     * is used by the conversion methods for number types. Note that the return
412     * value is not in always of the specified target class, but only if a new
413     * object has to be created.
414     *
415     * @param value the value to be converted (must not be <b>null</b>)
416     * @param targetClass the target class of the conversion (must be derived
417     * from {@code java.lang.Number})
418     * @return the converted number
419     * @throws ConversionException if the object cannot be converted
420     */
421    static Number toNumber(Object value, Class<?> targetClass) throws ConversionException
422    {
423        if (value instanceof Number)
424        {
425            return (Number) value;
426        }
427        else
428        {
429            String str = value.toString();
430            if (str.startsWith(HEX_PREFIX))
431            {
432                try
433                {
434                    return new BigInteger(str.substring(HEX_PREFIX.length()), HEX_RADIX);
435                }
436                catch (NumberFormatException nex)
437                {
438                    throw new ConversionException("Could not convert " + str
439                            + " to " + targetClass.getName()
440                            + "! Invalid hex number.", nex);
441                }
442            }
443
444            if (str.startsWith(BIN_PREFIX))
445            {
446                try
447                {
448                    return new BigInteger(str.substring(BIN_PREFIX.length()), BIN_RADIX);
449                }
450                catch (NumberFormatException nex)
451                {
452                    throw new ConversionException("Could not convert " + str
453                            + " to " + targetClass.getName()
454                            + "! Invalid binary number.", nex);
455                }
456            }
457
458            try
459            {
460                Constructor<?> constr = targetClass.getConstructor(CONSTR_ARGS);
461                return (Number) constr.newInstance(new Object[]{str});
462            }
463            catch (InvocationTargetException itex)
464            {
465                throw new ConversionException("Could not convert " + str
466                        + " to " + targetClass.getName(), itex
467                        .getTargetException());
468            }
469            catch (Exception ex)
470            {
471                // Treat all possible exceptions the same way
472                throw new ConversionException(
473                        "Conversion error when trying to convert " + str
474                                + " to " + targetClass.getName(), ex);
475            }
476        }
477    }
478
479    /**
480     * Convert the specified object into an URL.
481     *
482     * @param value the value to convert
483     * @return the converted value
484     * @throws ConversionException thrown if the value cannot be converted to an URL
485     */
486    public static URL toURL(Object value) throws ConversionException
487    {
488        if (value instanceof URL)
489        {
490            return (URL) value;
491        }
492        else if (value instanceof String)
493        {
494            try
495            {
496                return new URL((String) value);
497            }
498            catch (MalformedURLException e)
499            {
500                throw new ConversionException("The value " + value + " can't be converted to an URL", e);
501            }
502        }
503        else
504        {
505            throw new ConversionException("The value " + value + " can't be converted to an URL");
506        }
507    }
508
509    /**
510     * Convert the specified object into a Locale.
511     *
512     * @param value the value to convert
513     * @return the converted value
514     * @throws ConversionException thrown if the value cannot be converted to a Locale
515     */
516    public static Locale toLocale(Object value) throws ConversionException
517    {
518        if (value instanceof Locale)
519        {
520            return (Locale) value;
521        }
522        else if (value instanceof String)
523        {
524            List<String> elements = split((String) value, '_');
525            int size = elements.size();
526
527            if (size >= 1 && ((elements.get(0)).length() == 2 || (elements.get(0)).length() == 0))
528            {
529                String language = elements.get(0);
530                String country = (size >= 2) ? elements.get(1) : "";
531                String variant = (size >= 3) ? elements.get(2) : "";
532
533                return new Locale(language, country, variant);
534            }
535            else
536            {
537                throw new ConversionException("The value " + value + " can't be converted to a Locale");
538            }
539        }
540        else
541        {
542            throw new ConversionException("The value " + value + " can't be converted to a Locale");
543        }
544    }
545
546    /**
547     * Split a string on the specified delimiter. To be removed when
548     * commons-lang has a better replacement available (Tokenizer?).
549     *
550     * todo: replace with a commons-lang equivalent
551     *
552     * @param s          the string to split
553     * @param delimiter  the delimiter
554     * @param trim       a flag whether the single elements should be trimmed
555     * @return a list with the single tokens
556     */
557    public static List<String> split(String s, char delimiter, boolean trim)
558    {
559        if (s == null)
560        {
561            return new ArrayList<String>();
562        }
563
564        List<String> list = new ArrayList<String>();
565
566        StringBuilder token = new StringBuilder();
567        int begin = 0;
568        boolean inEscape = false;
569
570        while (begin < s.length())
571        {
572            char c = s.charAt(begin);
573            if (inEscape)
574            {
575                // last character was the escape marker
576                // can current character be escaped?
577                if (c != delimiter && c != LIST_ESC_CHAR)
578                {
579                    // no, also add escape character
580                    token.append(LIST_ESC_CHAR);
581                }
582                token.append(c);
583                inEscape = false;
584            }
585
586            else
587            {
588                if (c == delimiter)
589                {
590                    // found a list delimiter -> add token and resetDefaultFileSystem buffer
591                    String t = token.toString();
592                    if (trim)
593                    {
594                        t = t.trim();
595                    }
596                    list.add(t);
597                    token = new StringBuilder();
598                }
599                else if (c == LIST_ESC_CHAR)
600                {
601                    // eventually escape next character
602                    inEscape = true;
603                }
604                else
605                {
606                    token.append(c);
607                }
608            }
609
610            begin++;
611        }
612
613        // Trailing delimiter?
614        if (inEscape)
615        {
616            token.append(LIST_ESC_CHAR);
617        }
618        // Add last token
619        String t = token.toString();
620        if (trim)
621        {
622            t = t.trim();
623        }
624        list.add(t);
625
626        return list;
627    }
628
629    /**
630     * Split a string on the specified delimiter always trimming the elements.
631     * This is a shortcut for {@code split(s, delimiter, true)}.
632     *
633     * @param s          the string to split
634     * @param delimiter  the delimiter
635     * @return a list with the single tokens
636     */
637    public static List<String> split(String s, char delimiter)
638    {
639        return split(s, delimiter, true);
640    }
641
642    /**
643     * Escapes the delimiters that might be contained in the given string. This
644     * method works like {@link #escapeListDelimiter(String, char)}. In addition,
645     * a single backslash will also be escaped.
646     *
647     * @param s the string with the value
648     * @param delimiter the list delimiter to use
649     * @return the correctly escaped string
650     */
651    public static String escapeDelimiters(String s, char delimiter)
652    {
653        String s1 = StringUtils.replace(s, LIST_ESCAPE, LIST_ESCAPE + LIST_ESCAPE);
654        return escapeListDelimiter(s1, delimiter);
655    }
656
657    /**
658     * Escapes the list delimiter if it is contained in the given string. This
659     * method ensures that list delimiter characters that are part of a
660     * property's value are correctly escaped when a configuration is saved to a
661     * file. Otherwise when loaded again the property will be treated as a list
662     * property.
663     *
664     * @param s the string with the value
665     * @param delimiter the list delimiter to use
666     * @return the escaped string
667     * @since 1.7
668     */
669    public static String escapeListDelimiter(String s, char delimiter)
670    {
671        return StringUtils.replace(s, String.valueOf(delimiter), LIST_ESCAPE
672                + delimiter);
673    }
674
675    /**
676     * Convert the specified object into a Color. If the value is a String,
677     * the format allowed is (#)?[0-9A-F]{6}([0-9A-F]{2})?. Examples:
678     * <ul>
679     *   <li>FF0000 (red)</li>
680     *   <li>0000FFA0 (semi transparent blue)</li>
681     *   <li>#CCCCCC (gray)</li>
682     *   <li>#00FF00A0 (semi transparent green)</li>
683     * </ul>
684     *
685     * @param value the value to convert
686     * @return the converted value
687     * @throws ConversionException thrown if the value cannot be converted to a Color
688     */
689    public static Color toColor(Object value) throws ConversionException
690    {
691        if (value instanceof Color)
692        {
693            return (Color) value;
694        }
695        else if (value instanceof String && !StringUtils.isBlank((String) value))
696        {
697            String color = ((String) value).trim();
698
699            int[] components = new int[3];
700
701            // check the size of the string
702            int minlength = components.length * 2;
703            if (color.length() < minlength)
704            {
705                throw new ConversionException("The value " + value + " can't be converted to a Color");
706            }
707
708            // remove the leading #
709            if (color.startsWith("#"))
710            {
711                color = color.substring(1);
712            }
713
714            try
715            {
716                // parse the components
717                for (int i = 0; i < components.length; i++)
718                {
719                    components[i] = Integer.parseInt(color.substring(2 * i, 2 * i + 2), HEX_RADIX);
720                }
721
722                // parse the transparency
723                int alpha;
724                if (color.length() >= minlength + 2)
725                {
726                    alpha = Integer.parseInt(color.substring(minlength, minlength + 2), HEX_RADIX);
727                }
728                else
729                {
730                    alpha = Color.black.getAlpha();
731                }
732
733                return new Color(components[0], components[1], components[2], alpha);
734            }
735            catch (Exception e)
736            {
737                throw new ConversionException("The value " + value + " can't be converted to a Color", e);
738            }
739        }
740        else
741        {
742            throw new ConversionException("The value " + value + " can't be converted to a Color");
743        }
744    }
745
746    /**
747     * Convert the specified value into an internet address.
748     *
749     * @param value the value to convert
750     * @return the converted value
751     * @throws ConversionException thrown if the value cannot be converted to a InetAddress
752     *
753     * @since 1.5
754     */
755    static InetAddress toInetAddress(Object value) throws ConversionException
756    {
757        if (value instanceof InetAddress)
758        {
759            return (InetAddress) value;
760        }
761        else if (value instanceof String)
762        {
763            try
764            {
765                return InetAddress.getByName((String) value);
766            }
767            catch (UnknownHostException e)
768            {
769                throw new ConversionException("The value " + value + " can't be converted to a InetAddress", e);
770            }
771        }
772        else
773        {
774            throw new ConversionException("The value " + value + " can't be converted to a InetAddress");
775        }
776    }
777
778    /**
779     * Convert the specified value into an email address.
780     *
781     * @param value the value to convert
782     * @return the converted value
783     * @throws ConversionException thrown if the value cannot be converted to an email address
784     *
785     * @since 1.5
786     */
787    static Object toInternetAddress(Object value) throws ConversionException
788    {
789        if (value.getClass().getName().equals(INTERNET_ADDRESS_CLASSNAME))
790        {
791            return value;
792        }
793        else if (value instanceof String)
794        {
795            try
796            {
797                Constructor<?> ctor = Class.forName(INTERNET_ADDRESS_CLASSNAME)
798                        .getConstructor(new Class[] {String.class});
799                return ctor.newInstance(new Object[] {value});
800            }
801            catch (Exception e)
802            {
803                throw new ConversionException("The value " + value + " can't be converted to a InternetAddress", e);
804            }
805        }
806        else
807        {
808            throw new ConversionException("The value " + value + " can't be converted to a InternetAddress");
809        }
810    }
811
812    /**
813     * Calls Class.isEnum() on Java 5, returns false on older JRE.
814     */
815    static boolean isEnum(Class<?> cls)
816    {
817        return cls.isEnum();
818    }
819
820    /**
821     * Convert the specified value into a Java 5 enum.
822     *
823     * @param value the value to convert
824     * @param cls   the type of the enumeration
825     * @return the converted value
826     * @throws ConversionException thrown if the value cannot be converted to an enumeration
827     *
828     * @since 1.5
829     */
830    static <E extends Enum<E>> E toEnum(Object value, Class<E> cls) throws ConversionException
831    {
832        if (value.getClass().equals(cls))
833        {
834            return cls.cast(value);
835        }
836        else if (value instanceof String)
837        {
838            try
839            {
840                return Enum.valueOf(cls, (String) value);
841            }
842            catch (Exception e)
843            {
844                throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
845            }
846        }
847        else if (value instanceof Number)
848        {
849            try
850            {
851                E[] enumConstants = cls.getEnumConstants();
852                return enumConstants[((Number) value).intValue()];
853            }
854            catch (Exception e)
855            {
856                throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
857            }
858        }
859        else
860        {
861            throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
862        }
863    }
864
865    /**
866     * Convert the specified object into a Date.
867     *
868     * @param value  the value to convert
869     * @param format the DateFormat pattern to parse String values
870     * @return the converted value
871     * @throws ConversionException thrown if the value cannot be converted to a Calendar
872     */
873    public static Date toDate(Object value, String format) throws ConversionException
874    {
875        if (value instanceof Date)
876        {
877            return (Date) value;
878        }
879        else if (value instanceof Calendar)
880        {
881            return ((Calendar) value).getTime();
882        }
883        else if (value instanceof String)
884        {
885            try
886            {
887                return new SimpleDateFormat(format).parse((String) value);
888            }
889            catch (ParseException e)
890            {
891                throw new ConversionException("The value " + value + " can't be converted to a Date", e);
892            }
893        }
894        else
895        {
896            throw new ConversionException("The value " + value + " can't be converted to a Date");
897        }
898    }
899
900    /**
901     * Convert the specified object into a Calendar.
902     *
903     * @param value  the value to convert
904     * @param format the DateFormat pattern to parse String values
905     * @return the converted value
906     * @throws ConversionException thrown if the value cannot be converted to a Calendar
907     */
908    public static Calendar toCalendar(Object value, String format) throws ConversionException
909    {
910        if (value instanceof Calendar)
911        {
912            return (Calendar) value;
913        }
914        else if (value instanceof Date)
915        {
916            Calendar calendar = Calendar.getInstance();
917            calendar.setTime((Date) value);
918            return calendar;
919        }
920        else if (value instanceof String)
921        {
922            try
923            {
924                Calendar calendar = Calendar.getInstance();
925                calendar.setTime(new SimpleDateFormat(format).parse((String) value));
926                return calendar;
927            }
928            catch (ParseException e)
929            {
930                throw new ConversionException("The value " + value + " can't be converted to a Calendar", e);
931            }
932        }
933        else
934        {
935            throw new ConversionException("The value " + value + " can't be converted to a Calendar");
936        }
937    }
938
939    /**
940     * Returns an iterator over the simple values of a composite value. This
941     * implementation calls {@link #flatten(Object, char)} and
942     * returns an iterator over the returned collection.
943     *
944     * @param value the value to "split"
945     * @param delimiter the delimiter for String values
946     * @return an iterator for accessing the single values
947     */
948    public static Iterator<?> toIterator(Object value, char delimiter)
949    {
950        return flatten(value, delimiter, Collections.newSetFromMap(new IdentityHashMap<Object,Boolean>())).iterator();
951    }
952
953    /**
954     * Returns a collection with all values contained in the specified object.
955     * This method is used for instance by the {@code addProperty()}
956     * implementation of the default configurations to gather all values of the
957     * property to add. Depending on the type of the passed in object the
958     * following things happen:
959     * <ul>
960     * <li>Strings are checked for delimiter characters and split if necessary.</li>
961     * <li>For objects implementing the {@code Iterable} interface, the
962     * corresponding {@code Iterator} is obtained, and contained elements
963     * are added to the resulting collection.</li>
964     * <li>Arrays are treated as {@code Iterable} objects.</li>
965     * <li>All other types are directly inserted.</li>
966     * <li>Recursive combinations are supported, e.g. a collection containing
967     * an array that contains strings: The resulting collection will only
968     * contain primitive objects (hence the name &quot;flatten&quot;).</li>
969     * </ul>
970     *
971     * @param value the value to be processed
972     * @param delimiter the delimiter for String values
973     * @return a &quot;flat&quot; collection containing all primitive values of
974     *         the passed in object
975     */
976    private static Collection<?> flatten(Object value, char delimiter, final Set<Object> dejaVu)
977    {
978        dejaVu.add(value);
979        if (value instanceof String)
980        {
981            String s = (String) value;
982            if (s.indexOf(delimiter) > 0)
983            {
984                return split(s, delimiter);
985            }
986        }
987
988        Collection<Object> result = new LinkedList<Object>();
989        if (value instanceof Path)
990        {
991            // Don't handle as an Iterable.
992            result.add(value);
993        }
994        else if (value instanceof Iterable)
995        {
996            flattenIterator(result, ((Iterable<?>) value).iterator(), delimiter, dejaVu);
997        }
998        else if (value instanceof Iterator)
999        {
1000            flattenIterator(result, (Iterator<?>) value, delimiter, dejaVu);
1001        }
1002        else if (value != null)
1003        {
1004            if (value.getClass().isArray())
1005            {
1006                for (int len = Array.getLength(value), idx = 0; idx < len; idx++)
1007                {
1008                    result.addAll(flatten(Array.get(value, idx), delimiter, Collections.newSetFromMap(new IdentityHashMap<Object,Boolean>())));
1009                }
1010            }
1011            else
1012            {
1013                result.add(value);
1014            }
1015        }
1016
1017        return result;
1018    }
1019
1020    /**
1021     * Flattens the given iterator. For each element in the iteration
1022     * {@code flatten()} will be called recursively.
1023     *
1024     * @param target the target collection
1025     * @param it the iterator to process
1026     * @param delimiter the delimiter for String values
1027     */
1028    private static void flattenIterator(Collection<Object> target, Iterator<?> it, char delimiter, Set<Object> dejaVue)
1029    {
1030        while (it.hasNext())
1031        {
1032            final Object next = it.next();
1033            if (!dejaVue.contains(next))
1034            {
1035                target.addAll(flatten(next, delimiter, dejaVue));
1036            }
1037        }
1038    }
1039
1040    /**
1041     * Performs interpolation of the specified value. This method checks if the
1042     * given value contains variables of the form <code>${...}</code>. If
1043     * this is the case, all occurrences will be substituted by their current
1044     * values.
1045     *
1046     * @param value the value to be interpolated
1047     * @param config the current configuration object
1048     * @return the interpolated value
1049     */
1050    public static Object interpolate(Object value, AbstractConfiguration config)
1051    {
1052        if (value instanceof String)
1053        {
1054            return config.getSubstitutor().replace((String) value);
1055        }
1056        else
1057        {
1058            return value;
1059        }
1060    }
1061
1062    /**
1063     * Helper method for converting a value to a constant of an enumeration
1064     * class.
1065     *
1066     * @param enumClass the enumeration class
1067     * @param value the value to be converted
1068     * @return the converted value
1069     */
1070    @SuppressWarnings("unchecked")
1071    // conversion is safe because we know that the class is an Enum class
1072    private static Object convertToEnum(Class<?> enumClass, Object value)
1073    {
1074        return toEnum(value, enumClass.asSubclass(Enum.class));
1075    }
1076}