/*
 * 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeSet;

import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.MultiClassLoader;
import org.apache.openjpa.lib.util.Services;
import org.apache.openjpa.lib.util.StringUtil;

/**
 * Utilities for running product derivations.
 *
 * @author Abe White
 * @author Pinaki Poddar
 */
public class ProductDerivations {

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

    private static final ProductDerivation[] _derivations;
    private static final String[] _derivationNames;
    private static final Throwable[] _derivationErrors;
    private static String[] _prefixes;
    static {
        MultiClassLoader l = AccessController.doPrivileged(J2DoPrivHelper.newMultiClassLoaderAction());
        l.addClassLoader(0, AccessController
            .doPrivileged(J2DoPrivHelper.getClassLoaderAction(ProductDerivations.class)));
        l.addClassLoader(1, AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction()));
        _derivationNames = Services.getImplementors(ProductDerivation.class, l);
        _derivationErrors = new Throwable[_derivationNames.length];
        List<ProductDerivation> derivations =
            new ArrayList<>(_derivationNames.length);
        boolean errors = false;
        for (int i = 0; i < _derivationNames.length; i++) {
            try {
                ProductDerivation d = (ProductDerivation)
                    AccessController.doPrivileged(
                        J2DoPrivHelper.newInstanceAction(
                            Class.forName(_derivationNames[i], true, l)));
                d.validate();
                derivations.add(d);
            } catch (Throwable t) {
                if (t instanceof PrivilegedActionException)
                    t = ((PrivilegedActionException) t).getException();
                _derivationErrors[i] = t;
                errors = true;
            }
        }

        // must be at least one product derivation to define metadata factories,
        // etc.
        if (derivations.isEmpty()) {
            throw new MissingResourceException(_loc.get
                ("no-product-derivations", ProductDerivation.class.getName(),
                derivationErrorsToString()).getMessage(),
                ProductDerivations.class.getName(),"derivations");
        }

        // START - ALLOW PRINT STATEMENTS
        // if some derivations weren't instantiable, warn
        if (errors)
            System.err.println(_loc.get("bad-product-derivations",
                ProductDerivations.class.getName()));
        for (int i = 0; i < _derivationErrors.length; i++) {
            if (_derivationErrors[i] == null)
                continue;
            System.err.println(_derivationNames[i] + ":" +
                    _derivationErrors[i]);
            break;
        }
        // STOP - ALLOW PRINT STATEMENTS

        Collections.sort(derivations, new ProductDerivationComparator());
        _derivations =
            derivations.toArray(new ProductDerivation[derivations.size()]);

        List<String> prefixes = new ArrayList<>(2);
        prefixes.add("openjpa");
        for (ProductDerivation derivation : _derivations) {
            String prefix = derivation.getConfigurationPrefix();
            if (prefix != null && !"openjpa".equals(prefix))
                prefixes.add(prefix);
        }
        _prefixes = prefixes.toArray(new String[prefixes.size()]);
    }

    /**
     * Return all the product derivations registered in the current classloader
     */
    public static ProductDerivation[] getProductDerivations() {
        return _derivations;
    }

    /**
     * Return the recognized prefixes for configuration properties.
     */
    public static String[] getConfigurationPrefixes() {
        return _prefixes;
    }

    /**
     * Set the configuration prefix array. This is package-visible for
     * testing purposes.
     *
     * @since 0.9.7
     */
    static void setConfigurationPrefixes(String[] prefixes) {
        _prefixes = prefixes;
    }

    /**
     * Determine the full key name for <code>partialKey</code>, given the
     * registered prefixes and the entries in <code>map</code>. This method
     * computes the appropriate configuration prefix to use by looking
     * through <code>map</code> for a key starting with any of the known
     * configuration prefixes and ending with <code>partialKey</code> and, if a
     * value is found, using the prefix of that key. Otherwise, it uses
     * the first registered prefix.
     *
     * The given <code>partialKey</code> is first tested for containment in the
     * given map without any prefix.
     *
     * @since 0.9.7
     */
    public static String getConfigurationKey(String partialKey, Map map) {
        String firstKey = (map != null && map.containsKey(partialKey))
            ? partialKey : null;
        for (int i = 0; map != null && i < _prefixes.length; i++) {
            String fullKey = _prefixes[i] + "." + partialKey;
            if (map.containsKey(fullKey)) {
                if (firstKey == null)
                    firstKey = fullKey;
                else {
                    // if we've already found a property with a previous
                    // prefix, then this is a collision.
                    throw new IllegalStateException(_loc.get(
                        "dup-with-different-prefixes", firstKey, fullKey)
                        .getMessage());
                }
            }
        }

        if (firstKey == null)
            return _prefixes[0] + "." + partialKey;
        else
            return firstKey;
    }

    /**
     * Apply {@link ProductDerivation#beforeConfigurationConstruct} callbacks
     * to the the given instance. Exceptions other than fatal
     * {@link BootstrapException} are swallowed.
     */
    public static void beforeConfigurationConstruct(ConfigurationProvider cp) {
        for (ProductDerivation derivation : _derivations) {
            try {
                derivation.beforeConfigurationConstruct(cp);
            }
            catch (BootstrapException be) {
                if (be.isFatal())
                    throw be;
            }
            catch (Exception e) {
                // can't log; no configuration yet
                e.printStackTrace();
            }
        }
    }

    /**
     * Apply {@link ProductDerivation#beforeConfigurationLoad} callbacks
     * to the the given instance. Exceptions other than fatal
     * {@link BootstrapException} are swallowed.
     */
    public static void beforeConfigurationLoad(Configuration conf) {
        for (ProductDerivation derivation : _derivations) {
            try {
                derivation.beforeConfigurationLoad(conf);
            }
            catch (BootstrapException be) {
                if (be.isFatal())
                    throw be;
            }
            catch (Exception e) {
                // logging not configured yet
                e.printStackTrace();
            }
        }
    }

    /**
     * Apply {@link ProductDerivation#afterSpecificationSet} callbacks
     * to the the given instance. Exceptions other than fatal
     * {@link BootstrapException} are swallowed.
     */
    public static void afterSpecificationSet(Configuration conf) {
        for (ProductDerivation derivation : _derivations) {
            try {
                derivation.afterSpecificationSet(conf);
            }
            catch (BootstrapException be) {
                if (be.isFatal())
                    throw be;
            }
            catch (Exception e) {
                // logging not configured yet
                e.printStackTrace();
            }
        }
    }

    /**
     * Called as the first step of a Configuration's close() method.
     * Exceptions are swallowed.
     *
     * @since 0.9.7
     */
    public static void beforeClose(Configuration conf) {
        for (ProductDerivation derivation : _derivations) {
            try {
                derivation.beforeConfigurationClose(conf);
            }
            catch (Exception e) {
                conf.getConfigurationLog().warn(_loc.get("before-close-ex"), e);
            }
        }
    }

    /**
     * Load the given given resource, or return false if it is not a resource
     * this provider understands. The given class loader may be null.
     *
     * @param anchor optional named anchor within a multiple-configuration
     * resource
     */
    public static ConfigurationProvider load(String resource, String anchor,
        ClassLoader loader) {
        if (StringUtil.isEmpty(resource))
            return null;
        if (loader == null)
            loader = AccessController.doPrivileged(
                J2DoPrivHelper.getContextClassLoaderAction());
        ConfigurationProvider provider = null;
        StringBuilder errs = null;
        // most specific to least
        Throwable err = null;
        for (int i = _derivations.length - 1; i >= 0; i--) {
            try {
                provider = _derivations[i].load(resource, anchor, loader);
                if (provider != null)
                    return provider;
            } catch (Throwable t) {
                err = t;
                errs = (errs == null) ? new StringBuilder() : errs.append("\n");
                errs.append(_derivations[i].getClass().getName() + ":" + t);
            }
        }
        reportErrors(errs, resource, err);
        String rsrc = resource + "#" + anchor;
        MissingResourceException ex = new MissingResourceException(rsrc,
                ProductDerivations.class.getName(), rsrc);
        ex.initCause(err);
        throw ex;
    }

    /**
     * Load given file, or return false if it is not a file this provider
     * understands.
     *
     * @param anchor optional named anchor within a multiple-configuration file
     */
    public static ConfigurationProvider load(File file, String anchor,
        ClassLoader loader) {
        if (file == null)
            return null;
        if (loader == null)
            loader = AccessController.doPrivileged(
                J2DoPrivHelper.getContextClassLoaderAction());
        ConfigurationProvider provider = null;
        StringBuilder errs = null;
        Throwable err = null;
        // most specific to least
        for (int i = _derivations.length - 1; i >= 0; i--) {
            try {
                provider = _derivations[i].load(file, anchor);
                if (provider != null)
                    return provider;
            } catch (Throwable t) {
                err = t;
                errs = (errs == null) ? new StringBuilder() : errs.append("\n");
                errs.append(_derivations[i].getClass().getName() + ":" + t);
            }
        }
        String aPath = AccessController.doPrivileged(
            J2DoPrivHelper.getAbsolutePathAction(file));
        reportErrors(errs, aPath, err);
        String rsrc = aPath + "#" + anchor;
        MissingResourceException ex = new MissingResourceException(rsrc,
                ProductDerivations.class.getName(), rsrc);
        ex.initCause(err);
        throw ex;
    }

    /**
     * Return a {@link ConfigurationProvider} that has parsed system defaults.
     */
    public static ConfigurationProvider loadDefaults(ClassLoader loader) {
        return load(loader, false);
    }

    /**
     * Return a {@link ConfigurationProvider} that has parsed system globals.
     */
    public static ConfigurationProvider loadGlobals(ClassLoader loader) {
        return load(loader, true);
    }

    /**
     * Load a built-in resource location.
     */
    private static ConfigurationProvider load(ClassLoader loader,
       boolean globals) {
        if (loader == null)
            loader = AccessController.doPrivileged(
                J2DoPrivHelper.getContextClassLoaderAction());

        ConfigurationProvider provider = null;
        StringBuilder errs = null;
        String type = (globals) ? "globals" : "defaults";
        Throwable err = null;
        // most specific to least
        for (int i = _derivations.length - 1; i >= 0; i--) {
            try {
                provider = (globals) ? _derivations[i].loadGlobals(loader)
                    : _derivations[i].loadDefaults(loader);
                if (provider != null)
                   return provider;
            } catch (Throwable t) {
                err = t;
                errs = (errs == null) ? new StringBuilder() : errs.append("\n");
                errs.append(_derivations[i].getClass().getName() + ":" + t);
            }
        }
        reportErrors(errs, type, err);
        return null;
    }

    /**
     * Thrown proper exception for given errors.
     */
    private static void reportErrors(StringBuilder errs, String resource,
        Throwable nested) {
        if (errs == null)
            return;
        MissingResourceException ex = new MissingResourceException(errs.toString(),
                ProductDerivations.class.getName(), resource);
        ex.initCause(nested);
        throw ex;
    }

    /**
     * Return a List<String> of all the fully-qualified anchors specified in
     * <code>propertiesLocation</code>. The return values must be used in
     * conjunction with <code>propertiesLocation</code>. If there are no
     * product derivations or if no product derivations could find anchors,
     * this returns an empty list.
     *
     * @since 1.1.0
     */
    public static List<String> getFullyQualifiedAnchorsInPropertiesLocation(
        final String propertiesLocation) {
        List<String> fqAnchors = new ArrayList<>();
        StringBuilder errs = null;
        Throwable err = null;
        for (int i = _derivations.length - 1; i >= 0; i--) {
            try {
                if (propertiesLocation == null) {
                    String loc = _derivations[i].getDefaultResourceLocation();
                    addAll(fqAnchors, loc,
                        _derivations[i].getAnchorsInResource(loc));
                    continue;
                }

                File f = new File(propertiesLocation);
                if ((Boolean) J2DoPrivHelper.isFileAction(f).run()) {
                    addAll(fqAnchors, propertiesLocation,
                        _derivations[i].getAnchorsInFile(f));
                } else {
                    f = new File("META-INF" + File.separatorChar
                        + propertiesLocation);
                    if ((Boolean) J2DoPrivHelper.isFileAction(f).run()) {
                        addAll(fqAnchors, propertiesLocation,
                            _derivations[i].getAnchorsInFile(f));
                    } else {
                        addAll(fqAnchors, propertiesLocation,
                            _derivations[i].getAnchorsInResource(
                                propertiesLocation));
                    }
                }
            } catch (Throwable t) {
                err = t;
                errs = (errs == null) ? new StringBuilder() : errs.append("\n");
                errs.append(_derivations[i].getClass().getName() + ":" + t);
            }
        }
        reportErrors(errs, propertiesLocation, err);
        return fqAnchors;
    }

    private static void addAll(Collection collection, String base,
        Collection newMembers) {
        if (newMembers == null || collection == null)
            return;
        for (Object newMember : newMembers) {
            String fqLoc = base + "#" + newMember;
            if (!collection.contains(fqLoc))
                collection.add(fqLoc);
        }
    }


    public static Set<String> getSupportedQueryHints() {
        Set<String> result = new TreeSet<>();
        // most specific to least
        for (int i = _derivations.length - 1; i >= 0; i--) {
            Set<String> members = _derivations[i].getSupportedQueryHints();
            if (members != null && !members.isEmpty())
                result.addAll(members);
        }
        return result;
    }


    /**
     * Compare {@link ProductDerivation}s.
     */
    private static class ProductDerivationComparator
        implements Comparator<ProductDerivation> {

        @Override
        public int compare(ProductDerivation o1, ProductDerivation o2) {
            int type1 = o1.getType();
            int type2 = o2.getType();
            if (type1 != type2)
                return type1 - type2;

            // arbitrary but consistent order
            return o1.getClass().getName().compareTo(o2.getClass().
                getName());
		}
	}

    /**
     * Prints product derivation information.
     */
    public static void main(String[] args) {
        // START - ALLOW PRINT STATEMENTS
        System.err.println(derivationErrorsToString());
        // STOP - ALLOW PRINT STATEMENTS
    }

    /**
     * Return a message about the status of each product derivation.
     */
    private static String derivationErrorsToString() {
        StringBuilder buf = new StringBuilder();
        buf.append("ProductDerivations: ").append(_derivationNames.length);
        for (int i = 0; i < _derivationNames.length; i++) {
            buf.append("\n").append(i + 1).append(". ").
                append(_derivationNames[i]).append(": ");
            if (_derivationErrors[i] == null)
                buf.append("OK");
            else
                buf.append(_derivationErrors[i].toString());
        }
        return buf.toString();
    }
}

