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.configuration2.convert;
019
020import java.awt.Color;
021import java.io.File;
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.URI;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.net.UnknownHostException;
032import java.nio.file.Path;
033import java.nio.file.Paths;
034import java.text.ParseException;
035import java.text.SimpleDateFormat;
036import java.time.Duration;
037import java.time.format.DateTimeParseException;
038import java.util.Calendar;
039import java.util.Date;
040import java.util.Locale;
041import java.util.Objects;
042import java.util.regex.Pattern;
043import java.util.regex.PatternSyntaxException;
044
045import org.apache.commons.configuration2.ex.ConversionException;
046import org.apache.commons.lang3.BooleanUtils;
047import org.apache.commons.lang3.StringUtils;
048
049/**
050 * A utility class to convert the configuration properties into any type.
051 *
052 * @since 2.8.0
053 */
054public final class PropertyConverter {
055
056    /** Constant for the prefix of hex numbers. */
057    private static final String HEX_PREFIX = "0x";
058
059    /** Constant for the radix of hex numbers. */
060    private static final int HEX_RADIX = 16;
061
062    /** Constant for the prefix of binary numbers. */
063    private static final String BIN_PREFIX = "0b";
064
065    /** Constant for the radix of binary numbers. */
066    private static final int BIN_RADIX = 2;
067
068    /** Constant for the argument classes of the Number constructor that takes a String. */
069    private static final Class<?>[] CONSTR_ARGS = {String.class};
070
071    /** The fully qualified name of {@code javax.mail.internet.InternetAddress}, as used in the javamail-1.* API.  */
072    private static final String INTERNET_ADDRESS_CLASSNAME_JAVAX = "javax.mail.internet.InternetAddress";
073
074    /** The fully qualified name of {@code jakarta.mail.internet.InternetAddress}, as used in the javamail-2.0+ API. */
075    private static final String INTERNET_ADDRESS_CLASSNAME_JAKARTA = "jakarta.mail.internet.InternetAddress";
076
077    /**
078     * Converts a value to a constant of an enumeration class.
079     *
080     * @param enumClass the enumeration class
081     * @param value the value to be converted
082     * @return the converted value
083     */
084    @SuppressWarnings("unchecked")
085    // conversion is safe because we know that the class is an Enum class
086    private static Object convertToEnum(final Class<?> enumClass, final Object value) {
087        return toEnum(value, enumClass.asSubclass(Enum.class));
088    }
089
090    /**
091     * Converts the specified value object to the given target data class. If additional
092     * information is required for this conversion, it is obtained from the passed in {@code DefaultConversionHandler}
093     * object. If the class is a primitive type (Integer.TYPE, Boolean.TYPE, etc), the value returned will use the wrapper
094     * type (Integer.class, Boolean.class, etc).
095     *
096     * @param cls the target class of the converted value
097     * @param value the value to convert
098     * @param convHandler the conversion handler object
099     * @return the converted value
100     * @throws ConversionException if the value is not compatible with the requested type
101     */
102    public static Object to(final Class<?> cls, final Object value, final DefaultConversionHandler convHandler) throws ConversionException {
103        if (cls.isInstance(value)) {
104            return value; // no conversion needed
105        }
106
107        if (String.class.equals(cls)) {
108            return String.valueOf(value);
109        }
110        if (Boolean.class.equals(cls) || Boolean.TYPE.equals(cls)) {
111            return toBoolean(value);
112        }
113        if (Character.class.equals(cls) || Character.TYPE.equals(cls)) {
114            return toCharacter(value);
115        }
116        if (Number.class.isAssignableFrom(cls) || cls.isPrimitive()) {
117            if (Integer.class.equals(cls) || Integer.TYPE.equals(cls)) {
118                return toInteger(value);
119            }
120            if (Long.class.equals(cls) || Long.TYPE.equals(cls)) {
121                return toLong(value);
122            }
123            if (Byte.class.equals(cls) || Byte.TYPE.equals(cls)) {
124                return toByte(value);
125            }
126            if (Short.class.equals(cls) || Short.TYPE.equals(cls)) {
127                return toShort(value);
128            }
129            if (Float.class.equals(cls) || Float.TYPE.equals(cls)) {
130                return toFloat(value);
131            }
132            if (Double.class.equals(cls) || Double.TYPE.equals(cls)) {
133                return toDouble(value);
134            }
135            if (BigInteger.class.equals(cls)) {
136                return toBigInteger(value);
137            }
138            if (BigDecimal.class.equals(cls)) {
139                return toBigDecimal(value);
140            }
141            return toNumber(value, cls);
142        }
143        if (Date.class.equals(cls)) {
144            return toDate(value, convHandler.getDateFormat());
145        }
146        if (Calendar.class.equals(cls)) {
147            return toCalendar(value, convHandler.getDateFormat());
148        }
149        if (File.class.equals(cls)) {
150            return toFile(value);
151        }
152        if (Path.class.equals(cls)) {
153            return toPath(value);
154        }
155        if (URI.class.equals(cls)) {
156            return toURI(value);
157        }
158        if (URL.class.equals(cls)) {
159            return toURL(value);
160        }
161        if (Pattern.class.equals(cls)) {
162            return toPattern(value);
163        }
164        if (Locale.class.equals(cls)) {
165            return toLocale(value);
166        }
167        if (cls.isEnum()) {
168            return convertToEnum(cls, value);
169        }
170        if (Color.class.equals(cls)) {
171            return toColor(value);
172        }
173        if (cls.getName().equals(INTERNET_ADDRESS_CLASSNAME_JAVAX)) {
174            // javamail-1.* With javax.mail.* namespace.
175            return toInternetAddress(value, INTERNET_ADDRESS_CLASSNAME_JAVAX);
176        }
177        if (cls.getName().equals(INTERNET_ADDRESS_CLASSNAME_JAKARTA)) {
178            // javamail-2.0+, with jakarta.mail.* namespace.
179            return toInternetAddress(value, INTERNET_ADDRESS_CLASSNAME_JAKARTA);
180        }
181        if (InetAddress.class.isAssignableFrom(cls)) {
182            return toInetAddress(value);
183        }
184        if (Duration.class.equals(cls)) {
185            return toDuration(value);
186        }
187
188        throw new ConversionException("The value '" + value + "' (" + value.getClass() + ")" + " can't be converted to a " + cls.getName() + " object");
189    }
190
191    /**
192     * Converts the specified object into a BigDecimal.
193     *
194     * @param value the value to convert
195     * @return the converted value
196     * @throws ConversionException thrown if the value cannot be converted to a BigDecimal
197     */
198    public static BigDecimal toBigDecimal(final Object value) throws ConversionException {
199        final Number n = toNumber(value, BigDecimal.class);
200        if (n instanceof BigDecimal) {
201            return (BigDecimal) n;
202        }
203        return BigDecimal.valueOf(n.doubleValue());
204    }
205
206    /**
207     * Converts the specified object into a BigInteger.
208     *
209     * @param value the value to convert
210     * @return the converted value
211     * @throws ConversionException thrown if the value cannot be converted to a BigInteger
212     */
213    public static BigInteger toBigInteger(final Object value) throws ConversionException {
214        final Number n = toNumber(value, BigInteger.class);
215        if (n instanceof BigInteger) {
216            return (BigInteger) n;
217        }
218        return BigInteger.valueOf(n.longValue());
219    }
220
221    /**
222     * Converts the specified object into a Boolean. Internally the {@code org.apache.commons.lang.BooleanUtils} class from
223     * the <a href="https://commons.apache.org/lang/">Commons Lang</a> project is used to perform this conversion. This
224     * class accepts some more tokens for the boolean value of <strong>true</strong>, for example {@code yes} and {@code on}. Please refer to
225     * the documentation of this class for more details.
226     *
227     * @param value the value to convert
228     * @return the converted value
229     * @throws ConversionException thrown if the value cannot be converted to a boolean
230     */
231    public static Boolean toBoolean(final Object value) throws ConversionException {
232        if (value instanceof Boolean) {
233            return (Boolean) value;
234        }
235        if (!(value instanceof String)) {
236            throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
237        }
238        final Boolean b = BooleanUtils.toBooleanObject((String) value);
239        if (b == null) {
240            throw new ConversionException("The value " + value + " can't be converted to a Boolean object");
241        }
242        return b;
243    }
244
245    /**
246     * Converts the specified object into a Byte.
247     *
248     * @param value the value to convert
249     * @return the converted value
250     * @throws ConversionException thrown if the value cannot be converted to a byte
251     */
252    public static Byte toByte(final Object value) throws ConversionException {
253        final Number n = toNumber(value, Byte.class);
254        if (n instanceof Byte) {
255            return (Byte) n;
256        }
257        return n.byteValue();
258    }
259
260    /**
261     * Converts the specified object into a Calendar.
262     *
263     * @param value the value to convert
264     * @param format the DateFormat pattern to parse String values
265     * @return the converted value
266     * @throws ConversionException thrown if the value cannot be converted to a Calendar
267     */
268    public static Calendar toCalendar(final Object value, final String format) throws ConversionException {
269        if (value instanceof Calendar) {
270            return (Calendar) value;
271        }
272        if (value instanceof Date) {
273            final Calendar calendar = Calendar.getInstance();
274            calendar.setTime((Date) value);
275            return calendar;
276        }
277        if (!(value instanceof String)) {
278            throw new ConversionException("The value " + value + " can't be converted to a Calendar");
279        }
280        try {
281            final Calendar calendar = Calendar.getInstance();
282            calendar.setTime(new SimpleDateFormat(format).parse((String) value));
283            return calendar;
284        } catch (final ParseException e) {
285            throw new ConversionException("The value " + value + " can't be converted to a Calendar", e);
286        }
287    }
288
289    /**
290     * Converts the specified value object to a {@code Character}. This method converts the passed in object to a string. If
291     * the string has exactly one character, this character is returned as result. Otherwise, conversion fails.
292     *
293     * @param value the value to be converted
294     * @return the resulting {@code Character} object
295     * @throws ConversionException if the conversion is not possible
296     */
297    public static Character toCharacter(final Object value) throws ConversionException {
298        final String strValue = String.valueOf(value);
299        if (strValue.length() == 1) {
300            return Character.valueOf(strValue.charAt(0));
301        }
302        throw new ConversionException(String.format("The value '%s' cannot be converted to a Character object!", strValue));
303    }
304
305    /**
306     * Converts the specified object into a Color. If the value is a String, the format allowed is
307     * (#)?[0-9A-F]{6}([0-9A-F]{2})?. Examples:
308     * <ul>
309     * <li>FF0000 (red)</li>
310     * <li>0000FFA0 (semi transparent blue)</li>
311     * <li>#CCCCCC (gray)</li>
312     * <li>#00FF00A0 (semi transparent green)</li>
313     * </ul>
314     *
315     * @param value the value to convert
316     * @return the converted value
317     * @throws ConversionException thrown if the value cannot be converted to a Color
318     */
319    public static Color toColor(final Object value) throws ConversionException {
320        if (value instanceof Color) {
321            return (Color) value;
322        }
323        if (!(value instanceof String) || StringUtils.isBlank((String) value)) {
324            throw new ConversionException("The value " + value + " can't be converted to a Color");
325        }
326        String color = ((String) value).trim();
327
328        final int[] components = new int[3];
329
330        // check the size of the string
331        final int minlength = components.length * 2;
332        if (color.length() < minlength) {
333            throw new ConversionException("The value " + value + " can't be converted to a Color");
334        }
335
336        // remove the leading #
337        if (color.startsWith("#")) {
338            color = color.substring(1);
339        }
340
341        try {
342            // parse the components
343            for (int i = 0; i < components.length; i++) {
344                components[i] = Integer.parseInt(color.substring(2 * i, 2 * i + 2), HEX_RADIX);
345            }
346
347            // parse the transparency
348            final int alpha;
349            if (color.length() >= minlength + 2) {
350                alpha = Integer.parseInt(color.substring(minlength, minlength + 2), HEX_RADIX);
351            } else {
352                alpha = Color.black.getAlpha();
353            }
354
355            return new Color(components[0], components[1], components[2], alpha);
356        } catch (final Exception e) {
357            throw new ConversionException("The value " + value + " can't be converted to a Color", e);
358        }
359    }
360
361    /**
362     * Converts the specified object into a Date.
363     *
364     * @param value the value to convert
365     * @param format the DateFormat pattern to parse String values
366     * @return the converted value
367     * @throws ConversionException thrown if the value cannot be converted to a Calendar
368     */
369    public static Date toDate(final Object value, final String format) throws ConversionException {
370        if (value instanceof Date) {
371            return (Date) value;
372        }
373        if (value instanceof Calendar) {
374            return ((Calendar) value).getTime();
375        }
376        if (!(value instanceof String)) {
377            throw new ConversionException("The value " + value + " can't be converted to a Date");
378        }
379        try {
380            return new SimpleDateFormat(format).parse((String) value);
381        } catch (final ParseException e) {
382            throw new ConversionException("The value " + value + " can't be converted to a Date", e);
383        }
384    }
385
386    /**
387     * Converts the specified object into a Double.
388     *
389     * @param value the value to convert
390     * @return the converted value
391     * @throws ConversionException thrown if the value cannot be converted to a Double
392     */
393    public static Double toDouble(final Object value) throws ConversionException {
394        final Number n = toNumber(value, Double.class);
395        if (n instanceof Double) {
396            return (Double) n;
397        }
398        return Double.valueOf(n.doubleValue());
399    }
400
401    /**
402     * Converts the specified object into a Duration.
403     *
404     * @param value the value to convert
405     * @return the converted value
406     * @throws ConversionException thrown if the value cannot be converted to a Duration
407     * @since 2.8.0
408     */
409    public static Duration toDuration(final Object value) throws ConversionException {
410        if (value instanceof Duration) {
411            return (Duration) value;
412        }
413        if (value instanceof CharSequence) {
414            try {
415                return Duration.parse((CharSequence) value);
416            } catch (final DateTimeParseException e) {
417                throw new ConversionException("Could not convert " + value + " to Duration", e);
418            }
419        }
420        throw new ConversionException("The value " + value + " can't be converted to a Duration");
421    }
422
423    /**
424     * Converts the specified value into an {@link Enum}.
425     *
426     * @param value the value to convert
427     * @param cls the type of the enumeration
428     * @return the converted value
429     * @throws ConversionException thrown if the value cannot be converted to an enumeration
430     * @since 1.5
431     */
432    static <E extends Enum<E>> E toEnum(final Object value, final Class<E> cls) throws ConversionException {
433        if (value.getClass().equals(cls)) {
434            return cls.cast(value);
435        }
436        if (value instanceof String) {
437            try {
438                return Enum.valueOf(cls, (String) value);
439            } catch (final Exception e) {
440                throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
441            }
442        }
443        if (!(value instanceof Number)) {
444            throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
445        }
446        try {
447            final E[] enumConstants = cls.getEnumConstants();
448            return enumConstants[((Number) value).intValue()];
449        } catch (final Exception e) {
450            throw new ConversionException("The value " + value + " can't be converted to a " + cls.getName());
451        }
452    }
453
454    /**
455     * Converts the specified object into a File.
456     *
457     * @param value the value to convert
458     * @return the converted value
459     * @throws ConversionException thrown if the value cannot be converted to a File
460     * @since 2.3
461     */
462    public static File toFile(final Object value) throws ConversionException {
463        if (value instanceof File) {
464            return (File) value;
465        }
466        if (value instanceof Path) {
467            return ((Path) value).toFile();
468        }
469        if (value instanceof String) {
470            return new File((String) value);
471        }
472        throw new ConversionException("The value " + value + " can't be converted to a File");
473    }
474
475    /**
476     * Converts the specified object into a Float.
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 Float
481     */
482    public static Float toFloat(final Object value) throws ConversionException {
483        final Number n = toNumber(value, Float.class);
484        if (n instanceof Float) {
485            return (Float) n;
486        }
487        return Float.valueOf(n.floatValue());
488    }
489
490    /**
491     * Converts the specified value into an internet address.
492     *
493     * @param value the value to convert
494     * @return the converted value
495     * @throws ConversionException thrown if the value cannot be converted to a InetAddress
496     * @since 1.5
497     */
498    static InetAddress toInetAddress(final Object value) throws ConversionException {
499        if (value instanceof InetAddress) {
500            return (InetAddress) value;
501        }
502        if (!(value instanceof String)) {
503            throw new ConversionException("The value " + value + " can't be converted to a InetAddress");
504        }
505        try {
506            return InetAddress.getByName((String) value);
507        } catch (final UnknownHostException e) {
508            throw new ConversionException("The value " + value + " can't be converted to a InetAddress", e);
509        }
510    }
511
512    /**
513     * Converts the specified object into an Integer.
514     *
515     * @param value the value to convert
516     * @return the converted value
517     * @throws ConversionException thrown if the value cannot be converted to an integer
518     */
519    public static Integer toInteger(final Object value) throws ConversionException {
520        final Number n = toNumber(value, Integer.class);
521        if (n instanceof Integer) {
522            return (Integer) n;
523        }
524        return n.intValue();
525    }
526
527    /**
528     * Converts the specified value into an email address with the given class name.
529     *
530     * @param value the value to convert
531     * @param targetClassName the fully qualified name of the {@code InternetAddress} class to convert to, for example,
532     *      {@value #INTERNET_ADDRESS_CLASSNAME_JAVAX} or {@value #INTERNET_ADDRESS_CLASSNAME_JAKARTA}
533     * @return the converted value
534     * @throws ConversionException thrown if the value cannot be converted to an email address
535     * @since 1.5
536     */
537    static Object toInternetAddress(final Object value, final String targetClassName) throws ConversionException {
538        if (value.getClass().getName().equals(targetClassName)) {
539            return value;
540        }
541        if (!(value instanceof String)) {
542            throw new ConversionException("The value " + value + " can't be converted to an InternetAddress");
543        }
544        try {
545            final Constructor<?> ctor = Class.forName(targetClassName).getConstructor(String.class);
546            return ctor.newInstance(value);
547        } catch (final Exception e) {
548            throw new ConversionException("The value " + value + " can't be converted to an InternetAddress", e);
549        }
550    }
551
552    /**
553     * Converts the specified object into a Locale.
554     *
555     * @param value the value to convert
556     * @return the converted value
557     * @throws ConversionException thrown if the value cannot be converted to a Locale
558     */
559    public static Locale toLocale(final Object value) throws ConversionException {
560        if (value instanceof Locale) {
561            return (Locale) value;
562        }
563        if (!(value instanceof String)) {
564            throw new ConversionException("The value " + value + " can't be converted to a Locale");
565        }
566        final String[] elements = ((String) value).split("_");
567        final int size = elements.length;
568
569        if (size >= 1 && (elements[0].length() == 2 || elements[0].isEmpty())) {
570            final String language = elements[0];
571            final String country = size >= 2 ? elements[1] : "";
572            final String variant = size >= 3 ? elements[2] : "";
573
574            return new Locale(language, country, variant);
575        }
576        throw new ConversionException("The value " + value + " can't be converted to a Locale");
577    }
578
579    /**
580     * Converts the specified object into a Long.
581     *
582     * @param value the value to convert
583     * @return the converted value
584     * @throws ConversionException thrown if the value cannot be converted to a Long
585     */
586    public static Long toLong(final Object value) throws ConversionException {
587        final Number n = toNumber(value, Long.class);
588        if (n instanceof Long) {
589            return (Long) n;
590        }
591        return n.longValue();
592    }
593
594    /**
595     * Tries to convert the specified object into a number object. This method is used by the conversion methods for number
596     * types. Note that the return value is not in always of the specified target class, but only if a new object has to be
597     * created.
598     *
599     * @param value the value to be converted (must not be <strong>null</strong>)
600     * @param targetClass the target class of the conversion (must be derived from {@link Number})
601     * @return the converted number
602     * @throws ConversionException if the object cannot be converted
603     */
604    static Number toNumber(final Object value, final Class<?> targetClass) throws ConversionException {
605        if (value instanceof Number) {
606            return (Number) value;
607        }
608        final String str = Objects.toString(value, null);
609        if (StringUtils.startsWithAny(str, HEX_PREFIX)) {
610            try {
611                return new BigInteger(str.substring(HEX_PREFIX.length()), HEX_RADIX);
612            } catch (final NumberFormatException nex) {
613                throw new ConversionException("Could not convert " + str + " to " + targetClass.getName() + "! Invalid hex number.", nex);
614            }
615        }
616
617        if (StringUtils.startsWithAny(str, BIN_PREFIX)) {
618            try {
619                return new BigInteger(str.substring(BIN_PREFIX.length()), BIN_RADIX);
620            } catch (final NumberFormatException nex) {
621                throw new ConversionException("Could not convert " + str + " to " + targetClass.getName() + "! Invalid binary number.", nex);
622            }
623        }
624
625        try {
626            final Constructor<?> constr = targetClass.getConstructor(CONSTR_ARGS);
627            return (Number) constr.newInstance(str);
628        } catch (final InvocationTargetException itex) {
629            throw new ConversionException("Could not convert " + str + " to " + targetClass.getName(), itex.getTargetException());
630        } catch (final Exception ex) {
631            // Treat all possible exceptions the same way
632            throw new ConversionException("Conversion error when trying to convert " + str + " to " + targetClass.getName(), ex);
633        }
634    }
635
636    /**
637     * Converts the specified object into a Path.
638     *
639     * @param value the value to convert
640     * @return the converted value
641     * @throws ConversionException thrown if the value cannot be converted to a Path
642     * @since 2.3
643     */
644    public static Path toPath(final Object value) throws ConversionException {
645        if (value instanceof File) {
646            return ((File) value).toPath();
647        }
648        if (value instanceof Path) {
649            return (Path) value;
650        }
651        if (value instanceof String) {
652            return Paths.get((String) value);
653        }
654        throw new ConversionException("The value " + value + " can't be converted to a Path");
655    }
656
657    /**
658     * Converts the specified object into a Pattern.
659     *
660     * @param value the value to convert
661     * @return the converted value
662     * @throws ConversionException thrown if the value cannot be converted to a Pattern
663     */
664    public static Pattern toPattern(final Object value) throws ConversionException {
665        if (value instanceof Pattern) {
666            return (Pattern) value;
667        }
668        if (!(value instanceof String)) {
669            throw new ConversionException("The value " + value + " can't be converted to a Pattern");
670        }
671        try {
672            return Pattern.compile((String) value);
673        } catch (final PatternSyntaxException e) {
674            throw new ConversionException("The value " + value + " can't be converted to a Pattern", e);
675        }
676    }
677
678    /**
679     * Converts the specified object into a Short.
680     *
681     * @param value the value to convert
682     * @return the converted value
683     * @throws ConversionException thrown if the value cannot be converted to a short
684     */
685    public static Short toShort(final Object value) throws ConversionException {
686        final Number n = toNumber(value, Short.class);
687        if (n instanceof Short) {
688            return (Short) n;
689        }
690        return n.shortValue();
691    }
692
693    /**
694     * Converts the specified object into an URI.
695     *
696     * @param value the value to convert
697     * @return the converted value
698     * @throws ConversionException thrown if the value cannot be converted to an URI
699     */
700    public static URI toURI(final Object value) throws ConversionException {
701        if (value instanceof URI) {
702            return (URI) value;
703        }
704        if (!(value instanceof String)) {
705            throw new ConversionException("The value " + value + " can't be converted to an URI");
706        }
707        try {
708            return new URI((String) value);
709        } catch (final URISyntaxException e) {
710            throw new ConversionException("The value " + value + " can't be converted to an URI", e);
711        }
712    }
713
714    /**
715     * Converts the specified object into an URL.
716     *
717     * @param value the value to convert
718     * @return the converted value
719     * @throws ConversionException thrown if the value cannot be converted to an URL
720     */
721    public static URL toURL(final Object value) throws ConversionException {
722        if (value instanceof URL) {
723            return (URL) value;
724        }
725        if (!(value instanceof String)) {
726            throw new ConversionException("The value " + value + " can't be converted to an URL");
727        }
728        try {
729            return new URL((String) value);
730        } catch (final MalformedURLException e) {
731            throw new ConversionException("The value " + value + " can't be converted to an URL", e);
732        }
733    }
734
735    /**
736     * Private constructor prevents instances from being created.
737     */
738    private PropertyConverter() {
739        // presvents instantiation.
740    }
741}