/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.lib.conf;

import java.io.File;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.TreeSet;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.apache.openjpa.lib.util.collections.AbstractReferenceMap.ReferenceStrength;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.lib.util.ParseException;
import org.apache.openjpa.lib.util.StringDistance;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.lib.util.concurrent.ConcurrentReferenceHashMap;


/**
 * Utility methods dealing with configuration.
 *
 * @author Abe White
 */
public class Configurations {

    private static final Localizer _loc = Localizer.forPackage(Configurations.class);

    private static final ConcurrentReferenceHashMap _loaders = new
        ConcurrentReferenceHashMap(ReferenceStrength.WEAK, ReferenceStrength.HARD);

    private static final Object NULL_LOADER = "null-loader";

    public static final String CONFIG_RESOURCE_PATH = "configResourcePath";
    public static final String CONFIG_RESOURCE_ANCHOR = "configResourceAnchor";

    /**
     * Return the class name from the given plugin string, or null if none.
     */
    public static String getClassName(String plugin) {
        return getPluginComponent(plugin, true);
    }

    /**
     * Return the properties part of the given plugin string, or null if none.
     */
    public static String getProperties(String plugin) {
        return getPluginComponent(plugin, false);
    }

    /**
     * Return either the class name or properties string from a plugin string.
     */
    private static String getPluginComponent(String plugin, boolean clsName) {
        if (plugin != null)
            plugin = plugin.trim();
        if (StringUtil.isEmpty(plugin))
            return null;

        int openParen = -1;
        if (plugin.charAt(plugin.length() - 1) == ')')
            openParen = plugin.indexOf('(');
        if (openParen == -1) {
            int eq = plugin.indexOf('=');
            if (eq == -1)
                return (clsName) ? plugin : null;
            return (clsName) ? null : plugin;
        }

        // clsName(props) form
        if (clsName)
            return plugin.substring(0, openParen).trim();
        String prop = plugin.substring(openParen + 1,
            plugin.length() - 1).trim();
        return (prop.length() == 0) ? null : prop;
    }

    /**
     * Combine the given class name and properties into a plugin string.
     */
    public static String getPlugin(String clsName, String props) {
        if (StringUtil.isEmpty(clsName))
            return props;
        if (StringUtil.isEmpty(props))
            return clsName;
        return clsName + "(" + props + ")";
    }

    /**
     * Return a plugin string that combines the properties of the given plugin
     * strings, where properties of <code>override</code> will override the
     * same properties of <code>orig</code>.
     */
    public static String combinePlugins(String orig, String override) {
        if (StringUtil.isEmpty(orig))
            return override;
        if (StringUtil.isEmpty(override))
            return orig;

        String origCls = getClassName(orig);
        String overrideCls = getClassName(override);
        String cls;
        if (StringUtil.isEmpty(origCls))
            cls = overrideCls;
        else if (StringUtil.isEmpty(overrideCls))
            cls = origCls;
        else if (!origCls.equals(overrideCls))
            return override; // completely different plugin
        else
            cls = origCls;

        String origProps = getProperties(orig);
        String overrideProps = getProperties(override);
        if (StringUtil.isEmpty(origProps))
            return getPlugin(cls, overrideProps);
        if (StringUtil.isEmpty(overrideProps))
            return getPlugin(cls, origProps);

        Properties props = parseProperties(origProps);
        props.putAll(parseProperties(overrideProps));
        return getPlugin(cls, serializeProperties(props));
    }

    /**
     * Create the instance with the given class name, using the given
     * class loader. No configuration of the instance is performed by
     * this method.
     */
    public static Object newInstance(String clsName, ClassLoader loader) {
        return newInstance(clsName, null, null, loader, true);
    }

    /**
     * Create and configure an instance with the given class name and
     * properties as a String.
     */
    public static Object newInstance(String clsName, Configuration conf,
        String props, ClassLoader loader) {
        Object obj = newInstance(clsName, null, conf, loader, true);
        configureInstance(obj, conf, props);
        return obj;
    }

    /**
     * Create and configure an instance with the given class name and
     * properties.
     */
    public static Object newInstance(String clsName, Configuration conf,
        Properties props, ClassLoader loader) {
        Object obj = newInstance(clsName, null, conf, loader, true);
        configureInstance(obj, conf, props);
        return obj;
    }

    /**
     * Loads the given class name by the given loader.
     * For efficiency, a cache per class loader is maintained of classes already loader.
     * @param clsName
     * @param loader
     */
    static Class<?> loadClass(String clsName, ClassLoader loader) {
        Class<?> cls = null;
        Object key = loader == null ? NULL_LOADER : loader;
        Map<String,Class<?>> loaderCache = (Map<String,Class<?>>) _loaders.get(key);
        if (loaderCache == null) { // We don't have a cache for this loader.
            //OPENJPA-2636: Changed to HARD/WEAK to avoid Classloader leak:
            loaderCache = new ConcurrentReferenceHashMap(ReferenceStrength.HARD,
                ReferenceStrength.WEAK);
            _loaders.put(key, loaderCache);
        } else {  // We have a cache for this loader.
            cls = (Class<?>) loaderCache.get(clsName);
        }
        if (cls == null) {
            try {
                cls = ClassUtil.toClass(clsName, loader);
                loaderCache.put(clsName, cls);
            } catch (RuntimeException re) {
                // TODO, empty block is never good
            }
        }
        return cls;
    }

    /**
     * Helper method used by members of this package to instantiate plugin
     * values.
     */
    static Object newInstance(String clsName, Value val, Configuration conf,
        ClassLoader loader, boolean fatal) {
        if (StringUtil.isEmpty(clsName))
            return null;

        Class<?> cls = loadClass(clsName, findDerivedLoader(conf, loader));
        if (cls == null) {
        	cls = loadClass(clsName, findDerivedLoader(conf, null));
        }
        if (cls == null && conf.getUserClassLoader() != null) {
        	cls = loadClass(clsName, conf.getUserClassLoader());
        }

        if (cls == null) {
            if (fatal)
              throw getCreateException(clsName, val, new ClassNotFoundException(clsName));
            Log log = (conf == null) ? null : conf.getConfigurationLog();
	        if (log != null && log.isErrorEnabled())
	            log.error(_loc.get("plugin-creation-exception", val));
	        return null;
       }

        try {
            return AccessController.doPrivileged(J2DoPrivHelper.newInstanceAction(cls));
        } catch (Exception e) {
            if (e instanceof PrivilegedActionException) {
                e = ((PrivilegedActionException) e).getException();
            }
            RuntimeException re = new RuntimeException(_loc.get("obj-create", cls).getMessage(), e);
            if (fatal)
                throw re;
            Log log = (conf == null) ? null : conf.getConfigurationLog();
            if (log != null && log.isErrorEnabled())
                log.error(_loc.get("plugin-creation-exception", val), re);
            return null;
        }
    }

    /**
     * Attempt to find a derived loader that delegates to our target loader.
     * This allows application loaders that delegate appropriately for known
     * classes first crack at class names.
     */
    private static ClassLoader findDerivedLoader(Configuration conf, ClassLoader loader) {
        // we always prefer the thread loader, because it's the only thing we
        // can access that isn't bound to the OpenJPA classloader, unless
        // the conf object is of a custom class
        ClassLoader ctxLoader = AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction());
        if (loader == null) {
            if (ctxLoader != null) {
                return ctxLoader;
            } else if (conf != null) {
                return classLoaderOf(conf.getClass());
            } else {
            	return classLoaderOf(Configurations.class);
            }
        }

        for (ClassLoader parent = ctxLoader; parent != null; parent = parentClassLoaderOf(parent)) {
            if (parent == loader)
                return ctxLoader;
        }
        if (conf != null) {
            for (ClassLoader parent = classLoaderOf(conf.getClass()); parent != null;
                    parent = parentClassLoaderOf(parent)) {
                if (parent == loader)
                    return classLoaderOf(conf.getClass());
            }
        }
        return loader;
    }

    static ClassLoader classLoaderOf(Class<?> cls) {
    	return AccessController.doPrivileged(J2DoPrivHelper.getClassLoaderAction(cls));
    }

    static ClassLoader parentClassLoaderOf(ClassLoader loader) {
    	return AccessController.doPrivileged(J2DoPrivHelper.getParentAction(loader));
    }

    /**
     * Return a List<String> of all the fully-qualified anchors specified in the
     * properties location listed in <code>opts</code>. If no properties
     * location is listed in <code>opts</code>, this returns whatever the
     * product derivations can find in their default configurations.
     * If the properties location specified in <code>opts</code> already
     * contains an anchor spec, this returns that anchor. Note that in this
     * fully-qualified-input case, the logic involving product derivations
     * and resource parsing is short-circuited, so this method
     * should not be used as a means to test that a particular anchor is
     * defined in a given location by invoking with a fully-qualified anchor.
     *
     * This does not mutate <code>opts</code>.
     *
     * @since 1.1.0
     */
    public static List<String> getFullyQualifiedAnchorsInPropertiesLocation(
        Options opts) {
        String props = opts.getProperty("properties", "p", null);
        if (props != null) {
            int anchorPosition = props.indexOf("#");
            if (anchorPosition > -1)
                return Arrays.asList(new String[] { props });
        }

        return ProductDerivations.getFullyQualifiedAnchorsInPropertiesLocation(
            props);
    }

    /**
     * Set the given {@link Configuration} instance from the command line
     * options provided. All property names of the given configuration are
     * recognized; additionally, if a <code>properties</code> or
     * <code>p</code> argument exists, the resource it
     * points to will be loaded and set into the given configuration instance.
     * It can point to either a file or a resource name.
     */
    public static void populateConfiguration(Configuration conf, Options opts) {
        String props = opts.removeProperty("properties", "p", null);
        ConfigurationProvider provider;
        if (!StringUtil.isEmpty(props)) {
            Map<String, String> result = parseConfigResource(props);
            String path = result.get(CONFIG_RESOURCE_PATH);
            String anchor = result.get(CONFIG_RESOURCE_ANCHOR);

            File file = new File(path);
            if (AccessController.doPrivileged(J2DoPrivHelper
                    .isFileAction(file)))
                provider = ProductDerivations.load(file, anchor, null);
            else {
                file = new File("META-INF" + File.separatorChar + path);
                if (AccessController.doPrivileged(J2DoPrivHelper
                        .isFileAction(file)))
                    provider = ProductDerivations.load(file, anchor, null);
                else
                    provider = ProductDerivations.load(path, anchor, null);
            }
            if (provider != null)
                provider.setInto(conf);
            else
                throw new MissingResourceException(_loc.get("no-provider",
                    props).getMessage(), Configurations.class.getName(),
                    props);
        } else {
            provider = ProductDerivations.loadDefaults(null);
            if (provider != null)
                provider.setInto(conf);
        }
        opts.setInto(conf);
    }

    public static Map<String, String> parseConfigResource(String props) {
        String path = props;
        String anchor = null;
        int idx = path.lastIndexOf('#');
        if (idx != -1) {
            if (idx < path.length() - 1)
                anchor = path.substring(idx + 1);
            path = path.substring(0, idx);
            if (path.length() == 0)
                throw new MissingResourceException(_loc.get("anchor-only",
                    props).getMessage(), Configurations.class.getName(),
                    props);
        }
        Map <String, String> result = new HashMap<>();
        result.put(CONFIG_RESOURCE_PATH, path);
        result.put(CONFIG_RESOURCE_ANCHOR, anchor);
        return result;
    }

    /**
     * Helper method to throw an informative description on instantiation error.
     */
    private static RuntimeException getCreateException(String clsName, Value val, Exception e) {
        // re-throw the exception with some better information
        final String msg;
        final Object[] params;

        String alias = val.alias(clsName);
        String[] aliases = val.getAliases();
        String[] keys;
        if (aliases.length == 0)
            keys = aliases;
        else {
            keys = new String[aliases.length / 2];
            for (int i = 0; i < aliases.length; i += 2)
                keys[i / 2] = aliases[i];
        }

        String closest;
        if (keys.length == 0) {
            msg = "invalid-plugin";
            params = new Object[]{ val.getProperty(), alias, e.toString(), };
        } else if ((closest = StringDistance.getClosestLevenshteinDistance
            (alias, keys, 0.5f)) == null) {
            msg = "invalid-plugin-aliases";
            params = new Object[]{
                val.getProperty(), alias, e.toString(),
                new TreeSet<>(Arrays.asList(keys)), };
        } else {
            msg = "invalid-plugin-aliases-hint";
            params = new Object[]{
                val.getProperty(), alias, e.toString(),
                new TreeSet<>(Arrays.asList(keys)), closest, };
        }
        return new ParseException(_loc.get(msg, params), e);
    }

    /**
     * Configures the given object with the given properties by
     * matching the properties string to the object's setter
     * methods. The properties string should be in the form
     * "prop1=val1, prop2=val2 ...". Does not validate that setter
     * methods exist for the properties.
     *
     * @throws RuntimeException on configuration error
     */
    public static void configureInstance(Object obj, Configuration conf,
        String properties) {
        configureInstance(obj, conf, properties, null);
    }

    /**
     * Configures the given object with the given properties by
     * matching the properties string to the object's setter
     * methods. The properties string should be in the form
     * "prop1=val1, prop2=val2 ...". Validates that setter methods
     * exist for the properties.
     *
     * @throws RuntimeException on configuration error
     */
    public static void configureInstance(Object obj, Configuration conf,
        String properties, String configurationName) {
        if (obj == null)
            return;

        Properties props = null;
        if (!StringUtil.isEmpty(properties))
            props = parseProperties(properties);
        configureInstance(obj, conf, props, configurationName);
    }

    /**
     * Configures the given object with the given properties by
     * matching the properties string to the object's setter
     * methods. Does not validate that setter methods exist for the properties.
     *
     * @throws RuntimeException on configuration error
     */
    public static void configureInstance(Object obj, Configuration conf,
        Properties properties) {
        configureInstance(obj, conf, properties, null);
    }

    /**
     * Configures the given object with the given properties by
     * matching the properties string to the object's setter
     * methods. If <code>configurationName</code> is
     * non-<code>null</code>, validates that setter methods exist for
     * the properties.
     *
     * @throws RuntimeException on configuration error
     */
    public static void configureInstance(Object obj, Configuration conf,
        Properties properties, String configurationName) {
        if (obj == null)
            return;

        Options opts;
        if (properties instanceof Options)
            opts = (Options) properties;
        else {
            opts = new Options();
            if (properties != null)
                opts.putAll(properties);
        }

        Configurable configurable = null;
        if (conf != null && obj instanceof Configurable)
            configurable = (Configurable) obj;

        if (configurable != null) {
            configurable.setConfiguration(conf);
            configurable.startConfiguration();
        }
        Options invalidEntries = opts.setInto(obj);
        if (obj instanceof GenericConfigurable)
            ((GenericConfigurable) obj).setInto(invalidEntries);

		if (!invalidEntries.isEmpty() && configurationName != null) {
			Localizer.Message msg = null;
            String first = (String) invalidEntries.keySet().iterator().next();
			if (invalidEntries.keySet().size() == 1 &&
				first.indexOf('.') == -1) {
                // if there's just one misspelling and this is not a
				// path traversal, check for near misses.
                Collection<String> options = findOptionsFor(obj.getClass());
                String close = StringDistance.getClosestLevenshteinDistance
					(first, options, 0.75f);
				if (close != null)
                    msg = _loc.get("invalid-config-param-hint", new Object[]{
                            configurationName, obj.getClass(), first, close,
						    options, });
			}

            if (msg == null) {
                msg = _loc.get("invalid-config-params", new String[]{
                    configurationName, obj.getClass().getName(),
                    invalidEntries.keySet().toString(),
                    findOptionsFor(obj.getClass()).toString(), });
            }
            throw new ParseException(msg);
        }
        if (configurable != null)
            configurable.endConfiguration();
    }

    private static Collection<String> findOptionsFor(Class<?> cls) {
        Collection<String> c = Options.findOptionsFor(cls);

        // remove Configurable.setConfiguration() and
        // GenericConfigurable.setInto() from the set, if applicable.
        if (Configurable.class.isAssignableFrom(cls))
            c.remove("Configuration");
        if (GenericConfigurable.class.isAssignableFrom(cls))
            c.remove("Into");

        return c;
    }

    /**
     * Turn a set of properties into a comma-separated string.
     */
    public static String serializeProperties(Map map) {
        if (map == null || map.isEmpty())
            return null;

        StringBuilder buf = new StringBuilder();
        Map.Entry entry;
        String val;
        for (Object o : map.entrySet()) {
            entry = (Map.Entry) o;
            if (buf.length() > 0)
                buf.append(", ");
            buf.append(entry.getKey()).append('=');
            val = String.valueOf(entry.getValue());
            if (val.indexOf(',') != -1)
                buf.append('"').append(val).append('"');
            else
                buf.append(val);
        }
        return buf.toString();
    }

    /**
     * Parse a set of properties from a comma-separated string.
     */
    public static Options parseProperties(String properties) {
        Options opts = new Options();
        properties = StringUtil.trimToNull(properties);
        if (properties == null)
            return opts;

        try {
            String[] props = StringUtil.split(properties, ",", 0);
            int idx;
            char quote;
            String prop;
            String val;
            for (int i = 0; i < props.length; i++) {
                idx = props[i].indexOf('=');
                if (idx == -1) {
                    // if the key is not assigned to any value, set the
                    // value to the same thing as the key, and continue.
                    // This permits GenericConfigurable instances to
                    // behave meaningfully. We might consider setting the
                    // value to some well-known "value was not set, but
                    // key is present" string so that instances getting
                    // values injected can differentiate between a mentioned
                    // property and one set to a particular value.
                    prop = props[i];
                    val = prop;
                } else {
                    prop = props[i].substring(0, idx).trim();
                    val = props[i].substring(idx + 1).trim();
                }

                // if the value is quoted, read until the end quote
                if (((val.startsWith("\"") && val.endsWith("\""))
                    || (val.startsWith("'") && val.endsWith("'")))
                    && val.length() > 1)
                    val = val.substring(1, val.length() - 1);
                else if (val.startsWith("\"") || val.startsWith("'")) {
                    quote = val.charAt(0);
                    StringBuilder buf = new StringBuilder(val.substring(1));
                    int quotIdx;
                    while (++i < props.length) {
                        buf.append(",");

                        quotIdx = props[i].indexOf(quote);
                        if (quotIdx != -1) {
                            buf.append(props[i].substring(0, quotIdx));
                            if (quotIdx + 1 < props[i].length())
                                buf.append(props[i].substring(quotIdx + 1));
                            break;
                        } else
                            buf.append(props[i]);
                    }
                    val = buf.toString();
                }
                opts.put(prop, val);
            }
            return opts;
        } catch (RuntimeException re) {
            throw new ParseException(_loc.get("prop-parse", properties), re);
        }
    }

    /**
     * Looks up the given name in JNDI. If the name is null, null is returned.
     */
    public static Object lookup(String name, String userKey, Log log) {
        if (StringUtil.isEmpty(name))
            return null;

        Context ctx = null;
        try {
            ctx = new InitialContext();
            Object result = ctx.lookup(name);
            if (result == null && log != null && log.isWarnEnabled())
            	log.warn(_loc.get("jndi-lookup-failed", userKey, name));
            return result;
        } catch (NamingException ne) {
            throw new RuntimeException(
                _loc.get("naming-err", name).getMessage(), ne);
        } finally {
            if (ctx != null) {
                try {
                    ctx.close();
                } catch (NamingException ne) {
                    // ignore
                }
            }
        }
    }

    /**
     * Test whether the map contains the given partial key, prefixed with any
     * possible configuration prefix.
     */
    public static boolean containsProperty(Value value, Map props) {
        if (value == null || props == null || props.isEmpty())
            return false;
        List<String> partialKeys = value.getPropertyKeys();
        for (String partialKey : partialKeys) {
            if (props.containsKey(
                ProductDerivations.getConfigurationKey(partialKey, props)))
                return true;
        }
        return false;
    }

    /**
     * Test whether the map contains the given partial key, prefixed with any
     * possible configuration prefix.
     */
    public static boolean containsProperty(String partialKey, Map props) {
        if (partialKey == null || props == null || props.isEmpty())
            return false;
        else
            return props.containsKey(
                ProductDerivations.getConfigurationKey(partialKey, props));
    }

    /**
     * Get the property under the given partial key, prefixed with any possible
     * configuration prefix.
     */
    public static Object getProperty(String partialKey, Map m) {
        if (partialKey == null || m == null || m.isEmpty())
            return null;
        else
            return m.get(ProductDerivations.getConfigurationKey(partialKey, m));
    }

    /**
     * Remove the property under the given partial key, prefixed with any
     * possible configuration prefix.
     */
    public static Object removeProperty(String partialKey, Map props) {
        if (partialKey == null || props == null || props.isEmpty())
            return null;
        if (containsProperty(partialKey, props))
            return props.remove(ProductDerivations.getConfigurationKey(partialKey, props));
        else
            return null;
    }

    public static void removeProperty(String partialKey, Map<?,?> remaining, Map<?,?> props) {
        if (removeProperty(partialKey, remaining) != null) {
            removeProperty(partialKey, props);
        }
    }

    /**
     * Runs <code>runnable</code> against all the anchors in the configuration
     * pointed to by <code>opts</code>. Each invocation gets a fresh clone of
     * <code>opts</code> with the <code>properties</code> option set
     * appropriately.
     *
     * @since 1.1.0
     */
    public static boolean runAgainstAllAnchors(Options opts,
        Configurations.Runnable runnable) {
        if (opts.containsKey("help") || opts.containsKey("-help")) {
            return false;
        }
        List<String> anchors =
            Configurations.getFullyQualifiedAnchorsInPropertiesLocation(opts);

        // We use 'properties' below; get rid of 'p' to avoid conflicts. This
        // relies on knowing what getFullyQualifiedAnchorsInPropertiesLocation
        // looks for.
        if (opts.containsKey("p"))
            opts.remove("p");

        boolean ret = true;
        if (anchors.size() == 0) {
            ret = launchRunnable(opts, runnable);
        } else {
            for(String s : anchors ) {
                Options clonedOptions = (Options) opts.clone();
                clonedOptions.setProperty("properties", s);
                ret &= launchRunnable(clonedOptions, runnable);
            }
        }
        return ret;
    }

    private static boolean launchRunnable(Options opts,
        Configurations.Runnable runnable) {
        boolean ret = true;
        try {
            ret = runnable.run(opts);
        } catch (Exception e) {
            if (!(e instanceof RuntimeException))
                throw new RuntimeException(e);
            else
                throw (RuntimeException) e;
        }
        return ret;
    }

    public interface Runnable {
        boolean run(Options opts) throws Exception;
    }
}
