/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009-2013 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.loader;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.ow2.jonas.lib.loader.xml.FiltersDefinition;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.xmlconfig.XMLConfiguration;
import org.ow2.util.xmlconfig.XMLConfigurationException;
import org.ow2.util.xmlconfig.properties.SystemPropertyResolver;



/**
 * ClassLoader that allows to filter the classes or resources that are loaded.<br>
 * It is extending URLClassLoader as for example Tomcat is looking for getURLs() method, etc.
 * @author Florent Benoit
 */
public class FilteringClassLoader extends URLClassLoader {

    /**
     * Name of the XML file inside modules.
     */
    public static final String CLASSLOADER_FILTERING_FILE = "classloader-filtering.xml";

    /**
     * Name of the XML file.
     */
    public static final String XML_FILE = "classloader-default-filtering.xml";

    /**
     * Mapping file of the XML file.
     */
    public static final String MAPPING_FILE = "classloader-default-filtering-mapping.xml";


    /**
     * System property for disabling the Filtering.
     */
    public static final String DISABLE_FILTERING_PROPERTY_NAME = "jonas-disable-filtering-class-loader";

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(FilteringClassLoader.class);

    /**
     * If transparent, there will be no filtering (by default: filtering enabled).
     */
    private boolean transparent = false;

    /**
     * Lock to inhibits any change after the loader has been started.
     */
    private boolean started = false;

    /**
     * Url of the filtering definition.
     */
    private URL definitionUrl;

    /**
     * List of default filters.
     */
    private List<String> filters = null;

    /**
     * Build a new filtering classloader by using the given parent classloader.
     * @param parentClassLoader the given parent classloader
     */
    public FilteringClassLoader(final ClassLoader parentClassLoader) {
        super(new URL[0], parentClassLoader);
    }

    public boolean isTransparent() {
        return transparent;
    }

    public void setTransparent(final boolean transparent) {
        if (!started) {
            this.transparent = transparent;
        }
    }

    public URL getDefinitionUrl() {
        return definitionUrl;
    }

    public void setDefinitionUrl(final URL definitionUrl) {
        if (!started) {
            this.definitionUrl = definitionUrl;
        }
    }

    public void start() {
        this.started = true;

        // Allow to disable this filtering by the admin
        if (!transparent) {
            if (Boolean.getBoolean(DISABLE_FILTERING_PROPERTY_NAME)) {
                this.transparent = true;
                logger.debug("FilteringClassLoader has been disabled.");
            }
        }

        // Load default filters if option is enabled
        if (!transparent && (definitionUrl != null)) {
            loadFilters();
        }
    }

    /**
     * Load the default filters.
     */
    private void loadFilters() {

        // Parse with XML config
        XMLConfiguration xmlConfiguration = new XMLConfiguration(MAPPING_FILE);

        // Set the source configuration(s)
        xmlConfiguration.addConfigurationFile(definitionUrl);

        // Resolve properties as system properties
        xmlConfiguration.setPropertyResolver(new SystemPropertyResolver());

        // Use a map for some properties
        Map<String, Object> contextualInstances = new HashMap<String, Object>();

        // Sets the contextual instances
        xmlConfiguration.setContextualInstances(contextualInstances);

        // Start configuration of this object
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(FilteringClassLoader.class.getClassLoader());
            xmlConfiguration.configure(this);
        } catch (XMLConfigurationException e) {
            throw new IllegalArgumentException("Cannot configure the Filters inside the Filtering Classloader", e);
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }

        // check
        verifyFilters();

    }

    /**
     * Loads the class if it is not filtered.
     * @param name the name of the class.
     * @param resolve if true, resolve the class
     * @return The resulting Class object
     * @throws ClassNotFoundException If the class was not found or if it is
     *         filtered
     */
    @Override
    public Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {

        // If the class filtering is enabled and that the class is filtered, throws a CNFE exception
        if (isFiltered(name)) {
            throw new ClassNotFoundException("The class '" + name
                    + "' is a filtered class so it cannot be found in the Application Server classloader."
                    + " The class should be added into the application classloader.");
        }

        return super.loadClass(name, resolve);
    }

    /**
     * Find the given resource specified by its name.
     * @param name the resource name
     * @return the URL if the resource has been found, else null
     */
    @Override
    public URL getResource(final String name) {
        // If the resource filtering is enabled and that the resource is filtered, return null
        if (isFiltered(name)) {
            logger.debug("The resource ''{0}'' has been asked but as it is a filtered resource, return null", name);
            return null;
        }

        return super.getResource(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        // If the resource filtering is enabled and that the resource is filtered, return null
        if (isFiltered(name)) {
            logger.debug("The resource ''{0}'' has been asked but as it is a filtered resource, return empty Enumeration", name);
            List<URL> emptyList = Collections.emptyList();
            return Collections.enumeration(emptyList);
        }
        return super.getResources(name);
    }

    /**
     * Detects if the given class/resource should be filtered or not.
     * @param name the name of the class
     * @return true if it should be filtered, else false
     */
    protected boolean isFiltered(final String name) {
        if (transparent || name == null || filters == null) {
            return false;
        }

        // Finding a matching pattern
        for (String pattern : filters) {
            if (name.matches(pattern)) {
                return true;
            }
        }

        // Not filtered !
        return false;
    }

    /**
     * @return a cloned filter list (so that it cannot be changed after the start-up).
     */
    public List<String> getFilters() {
        List<String> clone = new ArrayList<String>();
        if (filters != null) {
            clone.addAll(filters);
        }
        return clone;
    }

    /**
     * Sets the filters definition element.
     * @param filtersDefinition the filters definition element.
     */
    public void setFiltersDefinition(final FiltersDefinition filtersDefinition) {
        this.filters = filtersDefinition.getFilters();
    }

    /**
     * Ensure that the filters are valid.
     */
    protected void verifyFilters() {

        // Check the list of filters
        if (filters != null) {
            Iterator<String> it = filters.iterator();
            while (it.hasNext()) {
                String filter = it.next();
                if ("*".equals(filter)) {
                    logger.warn("The '*' filter is not a valid value for a filter for definition file '" + definitionUrl + "'.");
                    it.remove();
                }

            }
        }
    }



}
