/*
 * Written by Dominik Drzewiecki and Dawid Kurzyniec and released to the public
 * domain, as explained at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.*;

/**
 * Property and string manipulation and macro expansion utilities.
 * Particularly useful in processing configuration files.
 * Macros enable configuration files to refer to system properties,
 * context-dependent information, etc.
 *
 * Macro expansion routine takes a string and resolves macro occurrences within
 * that string against specified macro templates. Macro references have the
 * following form: $macro_name{parameter}. Upon finding a macro reference,
 * resolver finds {@link Macro macro template} for <i>macro_name</i> and
 * uses it to resolve <i>parameter</i> into a value string. For instance,
 * empty macro name is associated by default with a template that resolves
 * system property names into their values. Thus, "${user.dir}" will be
 * replaced by the value of the system property "user.dir", and "${/}" will
 * be replaced by File.separator. As another example, "$trim{text}" will
 * resolve by default to the trimmed <i>text</i>.
 * <p>
 * Macro references may be nested. However, this version is vulnerable to
 * infinite recursion if expanded macros contain macro references themselves.
 * <p>
 * Importantly, the set of macro templates
 * is not fixed: users of this class can supply their custom macro templates
 * to the {@link #expand(String, Map)} method.
 * <p>
 * Usage examples of macro expansion:
 * <ul>
 * <li>
 * <code>"${h2o.home}"</code> - the directory pointed to by the system property
 *                              "h2o.home"
 * </li>
 * <li>
 * <code>"${h2o.home}${/}services"</code> - the "services"
 * subdirectory in the directory pointed to by the system property "h2o.home"
 * </li>
 * <li>
 * <code>"$fileurl{${h2o.home}${/}services}"</code> - the "services"
 * subdirectory in the directory pointed to by the system property "h2o.home",
 * converted to an URL
 * </li>
 * <li>
 * <code>"$fileurl{${h2o.home,${user.home}${/}.h2o}${/}services}"</code> -
 * the "services" subdirectory in the directory pointed to by the system
 * property "h2o.home" if set; otherwise, "services" directory in the ".h2o"
 * subdirectory in the default user dir; converted to an URL
 * </li>
 * </ul>
 */
public class PropertyUtils {
    /** Regular expression mathing macro patterns to be expanded */
    private final static String MACRO_PATTERN = "\\$([a-zA-Z_0-9\\.]*)\\{([^\\{\\}]*)\\}";

    /**
     * Macro template that expands the specified parameter in the
     * macro-specific way. For instance,
     * system property expansion macro interprets the parameter as the name of
     * a system property, and resolves to the value of that property.
     *
     * @author Dawid Kurzyniec
     * @version 1.0
     */
    public static interface Macro {

        /**
         * Expand the specified parameter input.
         *
         * @param input the macro input
         * @return the expanded string
         * @throws ExpansionException if expansion failed
         */
        String process(String input) throws ExpansionException;
    }

    /**
     * Utility macro which resolves system properties specified by name.
     * The input to the macro has the following syntax:
     * <pre>
     * name[,defaultValue]
     * </pre>
     * For instance: "temp.dir" and "temp.dir,/tmp" are both valid examples.
     * Additionally, two special-purpose names are supported:
     * "/" resolves to File.separator, and ":" resolves to
     * File.pathSeparator. This allows to use shortcuts like "${/}"
     * and "${:}" in configuration files.
     */
    public final static Macro MACRO_SYSTEM_PROPERTY = new PropertyExpansionMacro();

    /**
     * Utility macro which converts a file name into a URL. This macro properly
     * handles file names containing special characters such as ' ', '#', etc.
     * Note that valid URLs cannot contain spaces and other reserved
     * characters, which means that prepending "file:/" and replacing file
     * separators by slashes is NOT a valid conversion technique.
     * For instance:
     * <p>
     * <code>$fileurl{C:\Documents and Settings}/file</code> ->
     * <code>file:/C:/Documents%20and%20Settings/file</code>
     */
    public final static Macro MACRO_FILE_TO_URL = new Macro() {
        public String process(String input) {
            return new File(input).toURI().toString();
        }
    };

    /**
     * Utility macro that converts a file URL into a file path. The argument
     * must be a properly constructed URI (with no special characters etc).
     */
    public final static Macro MACRO_URL_TO_FILE = new Macro() {
        public String process(String input) throws ExpansionException {
            try {
                return new File(new URI(input)).getPath();
            }
            catch (URISyntaxException e) {
                throw new ExpansionException("Ill-formed URI", e);
            }
            catch (IllegalArgumentException e) {
                throw new ExpansionException("Not a local file URI", e);
            }
        }
    };

    /**
     * Utility macro that converts file path to absolute file path.
     */
    public final static Macro MACRO_ABSFILE = new Macro() {
        public String process(String input) {
            return new File(input).getAbsolutePath();
        }
    };

    /**
     * Utility macro which trims the specified argument, removing white spaces
     * from the beginning and end of it.
     */
    public final static Macro MACRO_TRIM = new Macro() {
        public String process(String input) {
            return input.trim();
        }
    };

    /**
     * Utility macro which converts its argument to upper case.
     */
    public final static Macro MACRO_UPPERCASE = new Macro() {
        public String process(String input) {
            return input.toUpperCase();
        }
    };

    /**
     * Utility macro which converts its argument to lower case.
     */
    public final static Macro MACRO_LOWERCASE = new Macro() {
        public String process(String input) {
            return input.toLowerCase();
        }
    };

    /**
     * Utility macro which converts its argument to the native library file
     * name using {@link System#mapLibraryName}.
     */
    public final static Macro MACRO_LIBNAME = new Macro() {
        public String process(String input) {
            return System.mapLibraryName(input);
        }
    };

    /**
     * Utility macro which converts its argument to the native executable file
     * name. For instance, on Windows it will convert "program" to
     * "program.exe".
     */
    public final static Macro MACRO_EXECUTABLE = new Macro() {
        public String process(String input) {
            String osname = System.getProperty("os.name").toLowerCase();
            if (osname.startsWith("windows")) {
                input += ".exe";
            }
            return input;
        }
    };

    private static final Map defaultMacros = new HashMap();
    static {
        defaultMacros.put("",           MACRO_SYSTEM_PROPERTY);
        defaultMacros.put("fileurl",    MACRO_FILE_TO_URL);
        defaultMacros.put("url2file",   MACRO_URL_TO_FILE);
        defaultMacros.put("absfile",    MACRO_ABSFILE);
        defaultMacros.put("trim",       MACRO_TRIM);
        defaultMacros.put("uppercase",  MACRO_UPPERCASE);
        defaultMacros.put("lowercase",  MACRO_LOWERCASE);
        defaultMacros.put("libname",    MACRO_LIBNAME);
        defaultMacros.put("executable", MACRO_EXECUTABLE);
    }

    /**
     * Returns default macros used by the {@link #expand} method. Currently,
     * it includes the following macros:
     * {@link #MACRO_SYSTEM_PROPERTY ""} (property expansion),
     * "{@link #MACRO_FILE_TO_URL fileurl}",
     * "{@link #MACRO_TRIM trim}",
     * "{@link #MACRO_UPPERCASE uppercase}",
     * "{@link #MACRO_LOWERCASE lowercase}",
     * "{@link #MACRO_LIBNAME libname}",
     * "{@link #MACRO_EXECUTABLE executable}".
     *
     * @return default macros used by the {@link #expand} method.
     */
    public static Map getDefaultMacros() {
        return Collections.unmodifiableMap(defaultMacros);
    }

    /** Lazy instantiated regular expression Pattern */
    private static Pattern macroPattern = null;

    private static synchronized Pattern getMacroExpansionPattern() {
        if (macroPattern == null) {
            macroPattern = Pattern.compile(MACRO_PATTERN);
        }
        return macroPattern;
    }

    /**
     * Iterates the properties and returns textual representaion
     *
     * @param p input properties
     *
     * @return properties textual representation
     */
    public final static String enumerate(Properties p) {
        StringBuffer sb = new StringBuffer(1024);
        Iterator i = p.keySet().iterator();
        String key;

        while (i.hasNext()) {
            key = (String) i.next();
            sb.append(key);
            sb.append('=');
            sb.append(p.getProperty(key));
            sb.append('\n');
        }

        return sb.toString();
    }

    /**
     * Tokenizes the properties provided as a string and returns plain
     * Properties
     *
     * @param propertiesString Properties string. Format:
     *        key1=value1;key2=value3;key3=value3
     *
     * @return Properties!
     */
    public final static Properties getPropertiesFromString(
        String propertiesString)
    {
        Properties p = new Properties();
        StringTokenizer st = new StringTokenizer(propertiesString, ";");
        String nameValuePair;
        String name;
        String value;
        int eqPos;

        while (st.hasMoreTokens()) {
            nameValuePair = st.nextToken();
            eqPos = nameValuePair.indexOf('=');

            if (eqPos != -1) {
                name = nameValuePair.substring(0, eqPos);
                value = nameValuePair.substring(eqPos + 1);
                p.put(name, value);
            }
        }

        return p;
    }

    /**
     * Reads properties from the provided input stream.
     *
     * @param is InputStream to read from
     * @return properties read
     * @throws IOException if I/O error occurs
     */
    public final static Properties getPropertiesFromStream(InputStream is)
        throws IOException
    {
        Properties p = new Properties();
        p.load(new BufferedInputStream(is));

        return p;
    }

    public static Set getKeysStartingWith(Properties props, String prefix) {
        Set keys = new HashSet(props.keySet());
        for (Iterator i = keys.iterator(); i.hasNext();) {
            String key = (String)i.next();
            if (!(key.startsWith(prefix))) i.remove();
        }
        return keys;
    }

    public static String escapeString(String string) {
        try {
            return URLEncoder.encode(string, "UTF-8");
        }
        catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 not supported");
        }
    }

    public static String unescapeString(String string) {
        try {
            return URLDecoder.decode(string, "UTF-8");
        }
        catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 not supported");
        }
    }

    /**
     * Expands provided string resolving macro templates using values of
     * {@link #getDefaultMacros default macros}.
     */
    public final static String expand(String value) throws ExpansionException {
        return expand(value, getDefaultMacros());
    }

    /**
     * Expands provided string resolving macro templates against supplied
     * macros. The macros map should map macro names (strings) to macro
     * objects (implementations of the {@link Macro} interface).
     */
    public final static String expand(String value, Map macros)
        throws ExpansionException
    {
        if (value == null) return null;

        // allocate StringBuffer large enough to avoid unnecessarry reallocation
        StringBuffer sb = new StringBuffer(value.length() * 4);
        sb.append(value);
        Matcher m;

        try {
            Pattern macroPattern = getMacroExpansionPattern();
            int occurrences;
            do {
                m = macroPattern.matcher(sb.toString());
                sb.setLength(0);
                occurrences = 0;
                while (m.find()) {
                    occurrences++;
                    String macroName = m.group(1);
                    String input = m.group(2);
                    Macro macro = (Macro) macros.get(macroName);
                    if (macro == null) {
                        throw new ExpansionException("Undefined macro: \"" +
                            macroName + "\"");
                    }
                    String replacement = macro.process(input);
                    if (replacement == null) replacement = "";
                    m.appendReplacement(sb, escapeReplacement(replacement));
                }
                m.appendTail(sb);
            }
            while (occurrences > 0);

        } catch (PatternSyntaxException pse) {
            // should *NOT* ever occur. We assume, that the regular expression syntax is OK.
            throw new RuntimeException(pse);
        }

        return sb.toString();
    }

    private static String escapeReplacement(String replacement) {
        StringBuffer buf = new StringBuffer(replacement.length()+10);
        int len = replacement.length();
        for (int i=0; i<len; i++) {
            char ch = replacement.charAt(i);
            switch (ch) {
                case '\\':
                    buf.append("\\\\");
                    break;
                case '$':
                    buf.append("\\$");
                    break;
                default:
                    buf.append(ch);
            }
        }
        return buf.toString();
    }

    /**
     * Property expansion template that resolves property names into values
     * using specified properties. If no properties are given to the
     * constructor, system properties are used.
     * The input to the macro has the following syntax:
     * <pre>
     * name[,defaultValue]
     * </pre>
     * For instance: "temp.dir,/tmp".
     * If this template resolves against system properties (e.g. if no-arg
     * constructor is used), two special-purpose names are supported:
     * "/" resolves to File.separator, and ":" resolves to
     * File.pathSeparator. This allows to use shortcuts like "${/}"
     * and "${:}" in configuration files.
     *
     * @author Dawid Kurzyniec
     * @version 1.0
     */
    public final static class PropertyExpansionMacro implements Macro {

        /** The properties to resolve against. */
        final Properties props;

        /**
         * Creates a new property expansion macro template that resolves
         * against system properties.
         */
        public PropertyExpansionMacro() {
            this(null);
        }

        /**
         * Creates a new property expansion macro template that resolves
         * against specified properties.
         *
         * @param props the properties to resolve against
         */
        public PropertyExpansionMacro(Properties props) {
            this.props = props;
        }

        /**
         * Returns property value for the specified property name, possibly
         * using specified default value.
         *
         * @param input the property name
         * @return the property value
         */
        public String process(String input) {
            if (props == null) {
                if ("/".equals(input)) {
                    return File.separator;
                }
                else if (":".equals(input)) {
                    return File.pathSeparator;
                }
            }
            // check if a default value was specified
            int i = input.indexOf(',');
            String pname = (i >= 0) ? input.substring(0, i) : input;
            String defVal = (i >= 0) ? input.substring(i+1) : null;

            if (props == null) {
                // note that the user may not be authorized to use
                // System.getProperties() although he may still be authorized
                // to pick individual system properties; hence, we keep the
                // null up to this point and never use System.getProperties()
                return System.getProperty(pname, defVal);
            }
            else {
                return props.getProperty(pname, defVal);
            }
        }
    }

    public static String getProperty(Properties props, String name, String defVal) {
        return props.getProperty(name, defVal);
    }

    public static void setProperty(Properties props, String name, String val) {
        props.setProperty(name, val);
    }

    public static void setOrRemoveProperty(Properties props, String name, String val) {
        if (val != null) {
            props.setProperty(name, val);
        }
        else {
            props.remove(name);
        }
    }

    public static Boolean getBooleanProperty(Properties props, String name) {
        String val = props.getProperty(name);
        if (val == null) return null;
        return Boolean.valueOf(
            val.equalsIgnoreCase("true") || val.equalsIgnoreCase("yes") ||
            val.equalsIgnoreCase("1"));
    }

    public static boolean getBooleanProperty(Properties props, String name, boolean defVal) {
        String val = props.getProperty(name, defVal ? "true" : "false");
        return (val.equalsIgnoreCase("true") || val.equalsIgnoreCase("yes") ||
                val.equalsIgnoreCase("1"));
    }

    public static void setBooleanProperty(Properties props, String name, boolean val) {
        props.setProperty(name, val ? "true" : "false");
    }

    public static Integer getIntegerProperty(Properties props, String name) {
        String val = props.getProperty(name);
        if (val == null) return null;
        return Integer.valueOf(val);
    }

    public static int getIntegerProperty(Properties props, String name, int defVal) {
        String val = props.getProperty(name);
        if (val == null) return defVal;
        return Integer.parseInt(val);
    }

    public static void setIntegerProperty(Properties props, String name, int val) {
        props.setProperty(name, String.valueOf(val));
    }


    public final static String uris2string(URI[] classpath) {
        StringBuffer buf = new StringBuffer(classpath.length * 30);
        for (int i=0; i<classpath.length; i++) {
            if (i>0) buf.append(' ');
            buf.append(classpath[i].toString());
        }
        return buf.toString();
    }

    public final static URI[] string2uris(String classpath) throws URISyntaxException {
        String[] split = classpath.split("\\s");
        URI[] uris = new URI[split.length];
        for (int i=0; i<split.length; i++) {
           uris[i] = new URI(split[i]);
        }
        return uris;
    }
}
