/*
 * 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.commons.chain2.web;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.apache.commons.chain2.Catalog;
import org.apache.commons.chain2.ChainUtil;
import org.apache.commons.chain2.config.xml.XmlConfigParser;
import org.apache.commons.chain2.impl.CatalogBase;
import org.apache.commons.chain2.impl.CatalogFactoryBase;
import org.apache.commons.chain2.web.servlet.ServletWebContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p><code>ServletContextListener</code> that automatically
 * scans chain configuration files in the current web application at
 * startup time, and exposes the result in a {@link Catalog} under a
 * specified servlet context attribute.  The following <em>context</em> init
 * parameters are utilized:</p>
 * <ul>
 * <li><strong>org.apache.commons.chain2.CONFIG_CLASS_RESOURCE</strong> -
 *     comma-delimited list of chain configuration resources to be loaded
 *     via <code>ClassLoader.getResource()</code> calls.  If not specified,
 *     no class loader resources will be loaded.</li>
 * <li><strong>org.apache.commons.chain2.CONFIG_WEB_RESOURCE</strong> -
 *     comma-delimited list of chain configuration webapp resources
 *     to be loaded.  If not specified, no web application resources
 *     will be loaded.</li>
 * <li><strong>org.apache.commons.chain2.CONFIG_ATTR</strong> -
 *     Name of the servlet context attribute under which the
 *     resulting {@link Catalog} will be created or updated.
 *     If not specified, it is expected that parsed resources will
 *     contain <code>&lt;catalog&gt;</code> elements (which will
 *     cause registration of the created {@link Catalog}s into
 *     the {@link CatalogFactoryBase} for this application, and no
 *     servlet context attribute will be created.
 *     <strong>NOTE</strong> - This parameter is deprecated.</p>
 * <li><strong>org.apache.commons.chain2.RULE_SET</strong> -
 *     Fully qualified class name of a Digester <code>RuleSet</code>
 *     implementation to use for parsing configuration resources (this
 *     class must have a public zero-args constructor).  If not defined,
 *     the standard <code>RuleSet</code> implementation will be used.</li>
 * </ul>
 *
 * <p>When a web application that has configured this listener is
 * started, it will acquire the {@link Catalog} under the specified servlet
 * context attribute key, creating a new one if there is none already there.
 * This {@link Catalog} will then be populated by scanning configuration
 * resources from the following sources (loaded in this order):</p>
 * <ul>
 * <li>Resources loaded from any <code>META-INF/chain-config.xml</code>
 *     resource found in a JAR file in <code>/WEB-INF/lib</code>.</li>
 * <li>Resources loaded from specified resource paths from the
 *     webapp's class loader (via <code>ClassLoader.getResource()</code>).</li>
 * <li>Resources loaded from specified resource paths in the web application
 *     archive (via <code>ServletContext.getResource()</code>).</li>
 * </ul>
 *
 * <p>If no attribute key is specified, on the other hand, parsed configuration
 * resources are expected to contain <code>&lt;catalog&gt;</code> elements,
 * and the catalogs will be registered with the {@link CatalogFactoryBase}
 * for this web application.</p>
 *
 * <p>This class requires Servlet 2.3 or later.  If you are running on
 * Servlet 2.2 system, consider using {@link ChainServlet} instead.
 * Note that {@link ChainServlet} uses parameters of the
 * same names, but they are <em>servlet</em> init parameters instead
 * of <em>context</em> init parameters.  Because of this, you can use
 * both facilities in the same application, if desired.</p>
 *
 * @version $Id$
 */
public class ChainListener implements ServletContextListener {

    // ------------------------------------------------------ Manifest Constants

    /**
     * <p>The name of the context init parameter containing the name of the
     * servlet context attribute under which our resulting {@link Catalog}
     * will be stored.</p>
     */
    public static final String CONFIG_ATTR =
        "org.apache.commons.chain2.CONFIG_ATTR";

    /**
     * <p>The name of the context init parameter containing a comma-delimited
     * list of class loader resources to be scanned.</p>
     */
    public static final String CONFIG_CLASS_RESOURCE =
        "org.apache.commons.chain2.CONFIG_CLASS_RESOURCE";

    /**
     * <p>The name of the context init parameter containing a comma-delimited
     * list of web application resources to be scanned.</p>
     */
    public static final String CONFIG_WEB_RESOURCE =
        "org.apache.commons.chain2.CONFIG_WEB_RESOURCE";

    /**
     * <p>The name of the context init parameter containing the fully
     * qualified class name of the <code>RuleSet</code> implementation
     * for configuring our {@link XmlConfigParser}.</p>
     */
    public static final String RULE_SET =
        "org.apache.commons.chain2.RULE_SET";

    /**
     * Constructor
     */
    public ChainListener() {
        CatalogFactoryBase.checkForValidConfigurationModule();
    }

    // ------------------------------------------ ServletContextListener Methods

    /**
     * <p>Remove the configured {@link Catalog} from the servlet context
     * attributes for this web application.</p>
     *
     * @param event <code>ServletContextEvent</code> to be processed
     */
    @Override
    public void contextDestroyed(final ServletContextEvent event) {
        ServletContext context = event.getServletContext();
        String attr = context.getInitParameter(CONFIG_ATTR);
        if (attr != null) {
            context.removeAttribute(attr);
        }
        CatalogFactoryBase.clear();
    }

    /**
     * <p>Scan the required chain configuration resources, assemble the
     * configured chains into a {@link Catalog}, and expose it as a
     * servlet context attribute under the specified key.</p>
     *
     * @param event <code>ServletContextEvent</code> to be processed
     */
    @Override
    public void contextInitialized(final ServletContextEvent event) {

        ServletContext context = event.getServletContext();

        // Retrieve context init parameters that we need
        String attr = context.getInitParameter(CONFIG_ATTR);

        // Retrieve or create the Catalog instance we may be updating
        Catalog<String, Object, ServletWebContext<String, Object>> catalog = null;
        if (attr != null) {
            Object catalogRef = context.getAttribute(attr);
            if (!(catalogRef instanceof Catalog)) {
                catalog = new CatalogBase<>();
            } else {
                /* Assume that we are getting an object of the type Catalog from the context's
                 * attribute because that is the historical contract. */
                catalog = ChainUtil.cast(catalogRef);
            }
        }

        // Construct the configuration resource parser we will use
        ClassLoader cl = (Thread.currentThread().getContextClassLoader() == null)
                ? this.getClass().getClassLoader() : Thread.currentThread().getContextClassLoader();

        String ruleSet = context.getInitParameter(RULE_SET);
        XmlConfigParser parser = (ruleSet == null)
                ? new XmlConfigParser() : new XmlConfigParser(ruleSet, cl);

        // Parse the resources specified in our init parameters (if any)
        parseJarResources(context, parser);
        ChainResources.parseClassResources(context.getInitParameter(CONFIG_CLASS_RESOURCE), parser);
        ChainResources.parseWebResources(
                context, context.getInitParameter(CONFIG_WEB_RESOURCE), parser);

        // Expose the completed catalog (if requested)
        if (attr != null) {
            context.setAttribute(attr, catalog);
        }
    }

    // --------------------------------------------------------- Private Methods

    /**
     * <p>Parse resources found in JAR files in the <code>/WEB-INF/lib</code>
     * subdirectory (if any).</p>
     * @param context <code>ServletContext</code> for this web application
     * @param parser {@link XmlConfigParser} to use for parsing
     */
    private void parseJarResources(final ServletContext context, final XmlConfigParser parser) {

        Set<String> jars = context.getResourcePaths("/WEB-INF/lib");
        if (jars == null) {
            jars = new HashSet<>();
        }

        Log log = LogFactory.getLog(ChainListener.class);
        for (final String jar : jars) {
            if (!jar.endsWith(".jar")) {
                continue;
            }

            final URL resourceURL;
            try {
                URL jarURL = context.getResource(jar);
                resourceURL = new URL("jar:"
                        + translate(jarURL.toExternalForm())
                        + "!/META-INF/chain-config.xml");

            } catch (final MalformedURLException e) {
                throw new RuntimeException(
                        String.format("Exception parsing chain config resource '%s':%s",
                                jar, e.getMessage()));
            }

            try (InputStream is = resourceURL.openStream()) {
                if (log.isDebugEnabled()) {
                    log.debug("Parsing: " + resourceURL);
                }
                parser.parse(resourceURL);

            } catch (final IOException e) {
                log.warn(e.getMessage());
            }
        }
    }

    /**
     * <p>Translate space character into <code>&pct;20</code> to avoid problems
     * with paths that contain spaces on some JVMs.</p>
     * @param value Value to translate
     * @return String
     */
    private String translate(final String value) {
        return value.replace(" ", "%20");
    }

}
