package org.apache.torque.generator.configuration.outlet;

/*
 * 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.
 */

import static org.apache.torque.generator.configuration.mergepoint.MergepointConfigurationTags.MERGEPOINT_TAG;
import static org.apache.torque.generator.configuration.outlet.OutletConfigurationTags.OUTLETSS_TAG;
import static org.apache.torque.generator.configuration.outlet.OutletConfigurationTags.OUTLET_TAG;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.torque.generator.configuration.ConfigurationEntityResolver;
import org.apache.torque.generator.configuration.ConfigurationHandlers;
import org.apache.torque.generator.configuration.ConfigurationProvider;
import org.apache.torque.generator.configuration.OutletTypes;
import org.apache.torque.generator.configuration.SaxHelper;
import org.apache.torque.generator.configuration.XMLConstants;
import org.apache.torque.generator.configuration.mergepoint.MergepointMapping;
import org.apache.torque.generator.configuration.mergepoint.MergepointSaxHandler;
import org.apache.torque.generator.configuration.paths.ProjectPaths;
import org.apache.torque.generator.outlet.Outlet;
import org.apache.torque.generator.qname.QualifiedName;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * A SAX handler which parses Outlet configuration files.
 * Relies on delegate handlers for parsing the configuration for
 * the different outlet types.
 */
public class OutletConfigurationSaxHandler extends DefaultHandler
{
    /** The access object for the configuration files, not null. */
    private ConfigurationProvider configurationProvider;

    /** The paths of the surrounding project, not null. */
    private ProjectPaths projectPaths;

    /** The list of outlets which configuration was already parsed. */
    private List<Outlet> outlets = new ArrayList<Outlet>();

    /**
     * The list of separate mergepoint mappings (outside the outlets)
     * which configuration was already parsed.
     */
    private List<MergepointMapping> mergepointMappings
            = new ArrayList<MergepointMapping>();

    /** The available configuration handlers. */
    private ConfigurationHandlers configurationHandlers;

    /**
     * The current delegate handler for parsing the current outlet's
     * configuration. Is null if no outlet is currently parsed.
     */
    private OutletSaxHandler outletHandler;

    /**
     * The current delegate handler for parsing the current mergepoint's
     * configuration. Is null if no mergepoint is currently parsed.
     */
    private MergepointSaxHandler mergepointHandler;

    /** The log. */
    private static Log log = LogFactory.getLog(OutletSaxHandler.class);

    /**
     * Constructor.
     *
     * @param configurationProvider The access object for the configuration
     *        files, not null.
     * @param projectPaths The paths of the surrounding project, not null.
     * @param configurationHandlers The available configuration handlers,
     *        not null.
     *
     * @throws NullPointerException if an argument is null.
     */
    public OutletConfigurationSaxHandler(
            ConfigurationProvider configurationProvider,
            ProjectPaths projectPaths,
            ConfigurationHandlers configurationHandlers)
    {
        if (configurationProvider == null)
        {
            log.error("OutletConfigurationSaxHandler: "
                    + " configurationProvider is null");
            throw new NullPointerException("configurationProvider is null");
        }
        if (projectPaths == null)
        {
            log.error("OutletConfigurationSaxHandler: "
                    + " projectPaths is null");
            throw new NullPointerException("projectPaths is null");
        }
        if (configurationHandlers == null)
        {
            log.error("OutletConfigurationSaxHandler: "
                    + " configurationHandlers is null");
            throw new NullPointerException("configurationHandlers is null");
        }
        this.configurationProvider = configurationProvider;
        this.projectPaths = projectPaths;
        this.configurationHandlers = configurationHandlers;
    }

    /**
     * Returns all outlets which were configured in the parsed outlet
     * configuration file.
     *
     * @return all created outlets, not null.
     */
    public List<Outlet> getOutlets()
    {
        return outlets;
    }

    /**
     * Returns all mergepoint mappings which were configured
     * outside the outlets in the parsed outlet configuration file.
     *
     * @return all created mergepoint mappings, not null.
     */
    public List<MergepointMapping> getMergepointMappings()
    {
        return mergepointMappings;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void startElement(String uri, String localName, String qName,
                             Attributes attributes)
            throws SAXException
    {
        String unqualifiedName = SaxHelper.getUnqualifiedName(localName, qName);
        if (outletHandler != null)
        {
            outletHandler.startElement(
                    uri, localName, qName, attributes);
        }
        else if (mergepointHandler != null)
        {
            mergepointHandler.startElement(
                    uri, localName, qName, attributes);
        }
        else if (OUTLET_TAG.equals(unqualifiedName))
        {
            String outletType = getOutletType(attributes);
            outletHandler = getOutletHandler(
                    null,
                    outletType);
            outletHandler.startElement(
                    uri, localName, qName, attributes);
        }
        else if (MERGEPOINT_TAG.equals(unqualifiedName))
        {
            mergepointHandler = new MergepointSaxHandler(
                    configurationProvider,
                    projectPaths,
                    configurationHandlers);
            mergepointHandler.startElement(
                    uri, localName, qName, attributes);
        }
        else if (!OUTLETSS_TAG.equals(unqualifiedName))
        {
            throw new SAXException(
                    "Unknown element : " + unqualifiedName
                    + ". First element must be "
                    + OUTLETSS_TAG
                    + " or "
                    + OUTLET_TAG);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void endElement(String uri, String localName, String rawName)
            throws SAXException
    {
        if (outletHandler != null)
        {
            outletHandler.endElement(uri, localName, rawName);
            if (outletHandler.isFinished())
            {
                Outlet outlet = outletHandler.getOutlet();
                outlets.add(outlet);
                if (log.isDebugEnabled())
                {
                    log.debug("Parsed configuration for the outlet "
                            + outlet.getName());
                }
                outletHandler = null;
            }
        }
        else if (mergepointHandler != null)
        {
            mergepointHandler.endElement(uri, localName, rawName);
            if (mergepointHandler.isFinished())
            {
                MergepointMapping mergepointMapping
                        = mergepointHandler.getMergepointMapping();
                mergepointMappings.add(mergepointMapping);
                if (log.isDebugEnabled())
                {
                    log.debug("Parsed configuration for the mergepoint "
                            + mergepointMapping.getName());
                }
                mergepointHandler = null;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void characters (char[] ch, int start, int length)
            throws SAXException
    {
        if (outletHandler != null)
        {
            outletHandler.characters(ch, start, length);
        }
    }

    /**
     * Returns the correct handler for a outlet tag.
     * The method uses the type attribute to determine which handler is needed.
     * I.e. it looks up the handler factory type in the outlet types,
     * and asks the factory for a handler.
     *
     * @param outletName the name for the outlet which configuration
     *        will be read in by the generated SaxHandlerFactory,
     *        or null if the name of the outlet should be determined from
     *        the parsed XML.
     * @param outletType the type of the outlet, not null.
     *
     * @return the handler for the tag, not null.
     *
     * @throws SAXException if no matching handler can be identified,
     *         or if an error occurs while creating the handler.
     */
    public OutletSaxHandler getOutletHandler(
            QualifiedName outletName,
            String outletType)
        throws SAXException
    {
        OutletTypes outletTypes = configurationHandlers.getOutletTypes();
        TypedOutletSaxHandlerFactory typedHandlerFactory
                = outletTypes.getTypedOutletHandlerFactories().get(outletType);
        if (typedHandlerFactory != null)
        {
            OutletSaxHandler outletSaxHandler
                    = typedHandlerFactory.getOutletSaxHandler(
                            outletName,
                            configurationProvider,
                            projectPaths,
                            configurationHandlers);
            return outletSaxHandler;
        }
        UntypedOutletSaxHandlerFactory untypedHandlerFactory = null;
        for (UntypedOutletSaxHandlerFactory candidate
                : outletTypes.getUntypedOutletHandlerFactories())
        {
            if (candidate.canHandle(outletType))
            {
                untypedHandlerFactory = candidate;
                break;
            }
        }
        if (untypedHandlerFactory == null)
        {
            throw new SAXException(
                    "Unknown outlet type: "
                        + outletType);
        }
        OutletSaxHandler outletSaxHandler
                = untypedHandlerFactory.getOutletSaxHandler(
                        outletType,
                        outletName,
                        configurationProvider,
                        projectPaths,
                        configurationHandlers);
        return outletSaxHandler;
    }

    /**
     * Reads the outlet type from the attributes of the outlet XML tag.
     *
     * @param attributes the attributes of the XML tag, not null.
     *
     * @return the outlet type, not null.
     *
     * @throws SAXException if the xsi:type attribute is not set.
     */
    public static String getOutletType(Attributes attributes)
            throws SAXException
    {
        String outletType
                = attributes.getValue(
                        XMLConstants.XSI_NAMESPACE,
                        XMLConstants.XSI_TYPE_ATTRBUTE_NAME);
        if (outletType == null)
        {
            throw new SAXException("The tag " + OUTLET_TAG
                    + " requires the attribute "
                    + XMLConstants.XSI_NAMESPACE
                    + ":"
                    + XMLConstants.XSI_TYPE_ATTRBUTE_NAME);
        }
        return outletType;
    }

    /**
     * EntityResolver implementation. Called by the XML parser
     *
     * @param publicId The public identifier of the external entity.
     * @param systemId The system identifier of the external entity.
     *
     * @return an InputSource for the entity, or null if the URI is not known.
     *
     * @see ConfigurationEntityResolver#resolveEntity(String, String)
     */
    public InputSource resolveEntity(String publicId, String systemId)
            throws SAXException, IOException
    {
        return new ConfigurationEntityResolver().resolveEntity(
                publicId, systemId);
    }

    @Override
    public void error(SAXParseException exception) throws SAXParseException
    {
        throw exception;
    }

    @Override
    public void fatalError(SAXParseException exception)
            throws SAXParseException
    {
        throw exception;
    }

    @Override
    public void warning(SAXParseException exception) throws SAXParseException
    {
        throw exception;
    }
}
