
/*
 * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
 * subject to license terms.
 */
package org.jdesktop.application;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A base class for converting arbitrary types to and from Strings, as well as 
 * a registry of ResourceConverter implementations.
 * <p>
 * The <tt>supportsType</tt> method defines what types a ResourceConverter supports.
 * By default it returns true for classes that are equal to the constructor's
 * <tt>type</tt> argument.  The <tt>parseType</tt> methods converts a string
 * the ResourceConverter's supported type, and the <tt>toString</tt> does the 
 * inverse, it converts a supported type to a String.  Concrete ResourceConverter 
 * subclasses must override <tt>parseType()</tt> and, in most cases, the 
 * <tt>toString</tt> method as well.
 * <p>
 * This class maintains a registry of ResourceConverters.  
 * The <tt>forType</tt> method returns the first ResourceConverter that 
 * supports a particular type, new ResourceConverters can be added with 
 * <tt>register()</tt>.  A small set of generic ResourceConverters are 
 * registered by default.  They support the following types:
 * <ul>
 * <li><tt>Boolean</tt></li>
 * <li><tt>Integer</tt></li>
 * <li><tt>Float</tt></li>
 * <li><tt>Double</tt></li>
 * <li><tt>Long</tt></li>
 * <li><tt>Short</tt></li>
 * <li><tt>Byte</tt></li>
 * <li><tt>MessageFormat</tt></li>
 * <li><tt>URL</tt></li>
 * <li><tt>URI</tt></li>
 * </ul>
 * <p>
 * The Boolean ResourceConverter returns true for "true", "on", "yes",
 * false otherwise.  The other primitive type ResourceConverters rely on
 * the corresponding static parse<i>Type</i> method,
 * e.g. <tt>Integer.parseInt()</tt>.  The MessageFormat
 * ResourceConverter just creates MessageFormat object with the string
 * as its constructor argument.  The URL/URI converters just apply
 * the corresponding constructor to the resource string.
 * 
 * @author Hans Muller (Hans.Muller@Sun.COM)
 * @see ResourceMap
 */
public abstract class ResourceConverter {

    protected final Class type;

    /**
     * Convert string to object
     * @param s the string to be parsed
     * @param r the {@code ResourceMap}
     * @return the object which was created from the string
     * @throws org.jdesktop.application.ResourceConverter.ResourceConverterException
     */
    public abstract Object parseString(String s, ResourceMap r)
            throws ResourceConverterException;

    /**
     * Null safe toString operation.
     * @param obj the object to be converted to String
     * @return result of obj.toString or "null"
     */
    public String toString(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    } //TODO: it should be static, or even moved to the utility class

    protected ResourceConverter(Class type) {
        if (type == null) {
            throw new IllegalArgumentException("null type");
        }
        this.type = type;
    }

    private ResourceConverter() {
        type = null;
    }  // not used

    /**
     * Checks whether {@code testType} can be converted with this converter.
     * @param testType
     * @return {@code true} if {@code testType} can be converted with this converter.
     */
    public boolean supportsType(Class testType) {
        return type.equals(testType);
    }

    public static class ResourceConverterException extends Exception {

        private final String badString;

        private String maybeShorten(String s) {
            int n = s.length();
            return (n < 128) ? s : s.substring(0, 128) + "...[" + (n - 128) + " more characters]";
        }

        public ResourceConverterException(String message, String badString, Throwable cause) {
            super(message, cause);
            this.badString = maybeShorten(badString);
        }

        public ResourceConverterException(String message, String badString) {
            super(message);
            this.badString = maybeShorten(badString);
        }

        @Override
        public String toString() {
            StringBuffer sb = new StringBuffer(super.toString());
            sb.append(" string: \"");
            sb.append(badString);
            sb.append("\"");
            return sb.toString();
        }
    }

    /**
     * Registers a {@code ResourceConverter}
     * @param resourceConverter the resource converter to be registered
     */
    public static void register(ResourceConverter resourceConverter) {
        if (resourceConverter == null) {
            throw new IllegalArgumentException("null resourceConverter");
        }
        resourceConverters.add(resourceConverter);
    }

    /**
     * Returns {@code ResourceConverter} for the specified type
     * @param type the type converter must be found for
     * @return the converter for specified type or {@code null} if no converter is found for the {@code type}
     */

    public static ResourceConverter forType(Class type) {
        if (type == null) {
            throw new IllegalArgumentException("null type");
        }
        for (ResourceConverter sc : resourceConverters) {
            if (sc.supportsType(type)) {
                return sc;
            }
        }
        return null;
    }
    private static ResourceConverter[] resourceConvertersArray = {
        new BooleanResourceConverter("true", "on", "yes"),
        new IntegerResourceConverter(),
        new MessageFormatResourceConverter(),
        new FloatResourceConverter(),
        new DoubleResourceConverter(),
        new LongResourceConverter(),
        new ShortResourceConverter(),
        new ByteResourceConverter(),
        new URLResourceConverter(),
        new URIResourceConverter()
    };
    private static List<ResourceConverter> resourceConverters =
            new ArrayList<ResourceConverter>(Arrays.asList(resourceConvertersArray));

    private static class BooleanResourceConverter extends ResourceConverter {

        private final String[] trueStrings;

        BooleanResourceConverter(String... trueStrings) {
            super(Boolean.class);
            this.trueStrings = trueStrings;
        }

        @Override
        public Object parseString(String s, ResourceMap ignore) {
            s = s.trim();
            for (String trueString : trueStrings) {
                if (s.equalsIgnoreCase(trueString)) {
                    return Boolean.TRUE;
                }
            }
            return Boolean.FALSE;
        }

        @Override
        public boolean supportsType(Class testType) {
            return testType.equals(Boolean.class) || testType.equals(boolean.class);
        }
    }

    private static abstract class NumberResourceConverter extends ResourceConverter {

        private final Class primitiveType;

        NumberResourceConverter(Class type, Class primitiveType) {
            super(type);
            this.primitiveType = primitiveType;
        }

        protected abstract Number parseString(String s) throws NumberFormatException;

        @Override
        public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
            try {
                return parseString(s);
            } catch (NumberFormatException e) {
                throw new ResourceConverterException("invalid " + type.getSimpleName(), s, e);
            }
        }

        @Override
        public boolean supportsType(Class testType) {
            return testType.equals(type) || testType.equals(primitiveType);
        }
    }

    private static class FloatResourceConverter extends NumberResourceConverter {

        FloatResourceConverter() {
            super(Float.class, float.class);
        }

        @Override
        protected Number parseString(String s) throws NumberFormatException {
            return Float.parseFloat(s);
        }
    }

    private static class DoubleResourceConverter extends NumberResourceConverter {

        DoubleResourceConverter() {
            super(Double.class, double.class);
        }

        @Override
        protected Number parseString(String s) throws NumberFormatException {
            return Double.parseDouble(s);
        }
    }

    private static abstract class INumberResourceConverter extends ResourceConverter {

        private final Class primitiveType;

        INumberResourceConverter(Class type, Class primitiveType) {
            super(type);
            this.primitiveType = primitiveType;
        }

        protected abstract Number parseString(String s, int radix) throws NumberFormatException;

        @Override
        public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
            try {
                String[] nar = s.split("&"); // number ampersand radix
                int radix = (nar.length == 2) ? Integer.parseInt(nar[1]) : -1;
                return parseString(nar[0], radix);
            } catch (NumberFormatException e) {
                throw new ResourceConverterException("invalid " + type.getSimpleName(), s, e);
            }
        }

        @Override
        public boolean supportsType(Class testType) {
            return testType.equals(type) || testType.equals(primitiveType);
        }
    }

    private static class ByteResourceConverter extends INumberResourceConverter {

        ByteResourceConverter() {
            super(Byte.class, byte.class);
        }

        @Override
        protected Number parseString(String s, int radix) throws NumberFormatException {
            return (radix == -1) ? Byte.decode(s) : Byte.parseByte(s, radix);
        }
    }

    private static class IntegerResourceConverter extends INumberResourceConverter {

        IntegerResourceConverter() {
            super(Integer.class, int.class);
        }

        @Override
        protected Number parseString(String s, int radix) throws NumberFormatException {
            return (radix == -1) ? Integer.decode(s) : Integer.parseInt(s, radix);
        }
    }

    private static class LongResourceConverter extends INumberResourceConverter {

        LongResourceConverter() {
            super(Long.class, long.class);
        }

        @Override
        protected Number parseString(String s, int radix) throws NumberFormatException {
            return (radix == -1) ? Long.decode(s) : Long.parseLong(s, radix);
        }
    }

    private static class ShortResourceConverter extends INumberResourceConverter {

        ShortResourceConverter() {
            super(Short.class, short.class);
        }

        @Override
        protected Number parseString(String s, int radix) throws NumberFormatException {
            return (radix == -1) ? Short.decode(s) : Short.parseShort(s, radix);
        }
    }

    private static class MessageFormatResourceConverter extends ResourceConverter {

        MessageFormatResourceConverter() {
            super(MessageFormat.class);
        }

        @Override
        public Object parseString(String s, ResourceMap ignore) {
            return new MessageFormat(s);
        }
    }

    private static class URLResourceConverter extends ResourceConverter {

        URLResourceConverter() {
            super(URL.class);
        }

        @Override
        public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
            try {
                return new URL(s);
            } catch (MalformedURLException e) {
                throw new ResourceConverterException("invalid URL", s, e);
            }
        }
    }

    private static class URIResourceConverter extends ResourceConverter {

        URIResourceConverter() {
            super(URI.class);
        }

        @Override
        public Object parseString(String s, ResourceMap ignore) throws ResourceConverterException {
            try {
                return new URI(s);
            } catch (URISyntaxException e) {
                throw new ResourceConverterException("invalid URI", s, e);
            }
        }
    }
}
