/*
 * 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.config.xml;

import java.io.IOException;
import java.net.URL;
import java.util.Map;

import org.apache.commons.chain2.CatalogFactory;
import org.apache.commons.chain2.config.ChainConfigurationException;
import org.apache.commons.chain2.config.ConfigParser;
import org.apache.commons.chain2.impl.CatalogFactoryBase;
import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.RuleSet;
import org.xml.sax.SAXException;

/**
 * <p>Class to parse the contents of an XML configuration file (using
 * Commons Digester) that defines and configures commands and command chains
 * to be registered in a {@link org.apache.commons.chain2.Catalog}.
 * Advanced users can configure the
 * detailed parsing behavior by configuring the properties of an instance
 * of this class prior to calling the <code>parse()</code> method.  It
 * is legal to call the <code>parse()</code> method more than once, in order
 * to parse more than one configuration document.</p>
 *
 * @version $Id$
 */
public class XmlConfigParser implements ConfigParser {

    // ----------------------------------------------------- Instance Variables

    /** <p>The <code>RuleSet</code> to be used for configuring our Digester parsing rules.</p> */
    private RuleSet ruleSet = new ConfigRuleSet();
    /** <p>Should Digester use the context class loader? */
    private boolean useContextClassLoader = true;

    // ------------------------------------------------------------- Constructor
    /** constructor */
    public XmlConfigParser() {
        super();
    }

    /**
     * @param rule String
     * @param loader String
     */
    public XmlConfigParser(final String rule, final ClassLoader loader) {
        if (rule == null) {
            throw new IllegalArgumentException(
                    "ConfigParser can't be instantiated with a null ruleSet class name");
        }
        if (loader == null) {
            throw new IllegalArgumentException(
                    "ConfigParser can't be instantiated with a null class loader reference");
        }

        try {
            Class<?> clazz = loader.loadClass(rule);
            setRuleSet((RuleSet) clazz.getDeclaredConstructor().newInstance());
        } catch (final ReflectiveOperationException e) {
            throw new RuntimeException(
                    String.format("Exception initializing RuleSet '%s' instance: %s",
                            rule, e.getMessage()));
        }
    }

    // ------------------------------------------------------------- Properties

    /**
     * <p>Return the <code>Digester</code> instance to be used for
     * parsing, creating one if necessary.</p>
     * @return A Digester instance.
     */
    public Digester getDigester() {
        Digester digester = new Digester();
        RuleSet set = getRuleSet();
        digester.setNamespaceAware(set.getNamespaceURI() != null);
        digester.setUseContextClassLoader(getUseContextClassLoader());
        digester.setValidating(false);
        digester.addRuleSet(set);
        return digester;
    }

    /**
     * <p>Return the <code>RuleSet</code> to be used for configuring
     * our <code>Digester</code> parsing rules, creating one if necessary.</p>
     * @return The RuleSet for configuring a Digester instance.
     */
    public RuleSet getRuleSet() {
        return this.ruleSet;
    }

    /**
     * <p>Set the <code>RuleSet</code> to be used for configuring
     * our <code>Digester</code> parsing rules.</p>
     *
     * @param set The new RuleSet to use
     */
    public void setRuleSet(final RuleSet set) {
        this.ruleSet = set;
    }

    /**
     * <p>Return the "use context class loader" flag.  If set to
     * <code>true</code>, Digester will attempt to instantiate new
     * command and chain instances from the context class loader.</p>
     * @return <code>true</code> if Digester should use the context class loader.
     */
    public boolean getUseContextClassLoader() {
        return this.useContextClassLoader;
    }

    /**
     * <p>Set the "use context class loader" flag.</p>
     *
     * @param val The new flag value
     */
    public void setUseContextClassLoader(final boolean val) {
        this.useContextClassLoader = val;
    }

    // --------------------------------------------------------- Public Methods

    /**
     * <p>Parse the XML document at the specified URL using the configured
     * <code>RuleSet</code>, registering catalogs with nested chains and
     * commands as they are encountered.  Use this method <strong>only</strong>
     * if you have included one or more <code>factory</code> elements in your
     * configuration resource.</p>
     *
     * @param <K> the type of keys maintained by the context associated with this command
     * @param <V> the type of mapped values
     * @param <C> Type of the context associated with this command
     * @param url <code>URL</code> of the XML document to be parsed
     * @return a CatalogFactory instance parsed from the given location
     * @throws ChainConfigurationException if a parsing error occurs
     */
    @Override
    public <K, V, C extends Map<K, V>> CatalogFactory<K, V, C> parse(final URL url) {
        // Prepare our Digester instance
        Digester digester = getDigester();
        digester.clear();

        // Parse the configuration document
        try {
            digester.parse(url);
        } catch (final IOException | SAXException e) {
            String msg = String.format(
                    "Error parsing digester configuration at url: %s",
                    url);
            throw new ChainConfigurationException(msg, e);
        }
        // FIXME get rid of singleton pattern and create a new instance here
        return CatalogFactoryBase.getInstance();
    }

}
