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

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.SynapseException;
import org.apache.synapse.config.SynapseConfiguration;
import org.apache.synapse.config.XMLToObjectMapper;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;

/**
 * This class is based on J2SE Service Provider model
 * http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#Service%20Provider
 * <p/>
 * It deals with both the problem of turning an XML into a Synapse config and vice-versa
 */
public class ConfigurationFactoryAndSerializerFinder implements XMLToObjectMapper {

    private static final Log log = LogFactory
            .getLog(ConfigurationFactoryAndSerializerFinder.class);

    private static final Class[] configurationFactories = {
            SynapseXMLConfigurationFactory.class,
    };


    private final static ConfigurationFactoryAndSerializerFinder instance =
            new ConfigurationFactoryAndSerializerFinder();

    /**
     * A map of mediator QNames to implementation class
     */
    private final static Map<QName,Class> factoryMap = new HashMap<QName,Class>();

    private final static Map<QName,Class> serializerMap = new HashMap<QName,Class>();

    private static boolean initialized = false;

    public static synchronized ConfigurationFactoryAndSerializerFinder getInstance() {
        if (!initialized) {
            loadConfigurationFatoriesAndSerializers();
        }
        return instance;
    }

    /**
     * Force re initialization next time
     */
    public static synchronized void reset() {
        factoryMap.clear();
        serializerMap.clear();
        initialized = false;
    }

    private ConfigurationFactoryAndSerializerFinder() {
    }

    private void handleException(String msg) {
        log.error(msg);
        throw new SynapseException(msg);
    }

    private static void loadConfigurationFatoriesAndSerializers() {
        for (Class c : configurationFactories) {
            if (c != null) {
                try {
                    ConfigurationFactory fac = (ConfigurationFactory) c.newInstance();
                    factoryMap.put(fac.getTagQName(), c);
                    serializerMap.put(fac.getTagQName(), fac.getSerializerClass());
                } catch (Exception e) {
                    throw new SynapseException("Error instantiating " + c.getName(), e);
                }
            }
        }
        // now iterate through the available plugable mediator factories
        registerExtensions();
        initialized = true;
    }
    /**
     * Register pluggable mediator factories from the classpath
     * <p/>
     * This looks for JAR files containing a META-INF/services that adheres to the following
     * https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
     */
    private static void registerExtensions() {

        Iterator<ConfigurationFactory> factories = ServiceLoader.load(ConfigurationFactory.class).iterator();
        // register MediatorFactory extensions
        while (factories.hasNext()) {
            ConfigurationFactory factory = factories.next();
            QName tag = factory.getTagQName();
            factoryMap.put(tag, factory.getClass());
            serializerMap.put(tag, factory.getSerializerClass());
            if (log.isDebugEnabled()) {
                log.debug("Added MediatorFactory " + factory.getClass() + " to handle " + tag);
            }
        }
    }

    /**
     * This method returns a Processor given an OMElement. This will be used
     * recursively by the elements which contain processor elements themselves
     * (e.g. rules)
     *
     * @param element
     * @return Processor
     */
    public SynapseConfiguration getConfiguration(OMElement element, Properties properties) {

        String localName = element.getLocalName();
        QName qName;
        if (element.getNamespace() != null) {
            qName = new QName(element.getNamespace().getNamespaceURI(), localName);
        } else {
            qName = new QName(localName);
        }
        if (log.isDebugEnabled()) {
            log.debug("getConfiguration(" + qName + ")");
        }
        Class cls = factoryMap.get(qName);


        if (cls == null) {
            String msg = "Unknown Configuration type " +
                    "referenced by configuration element : " + qName;
            log.error(msg);
            throw new SynapseException(msg);
        }

        try {
            ConfigurationFactory cf = (ConfigurationFactory) cls.newInstance();
            return cf.getConfiguration(element, properties);

        } catch (InstantiationException e) {
            String msg = "Error initializing configuration factory : " + cls;
            log.error(msg);
            throw new SynapseException(msg, e);

        } catch (IllegalAccessException e) {
            String msg = "Error initializing configuration factory : " + cls;
            log.error(msg);
            throw new SynapseException(msg, e);
        }
    }

    /**
     * @param synCfg
     * @return
     */
    public static OMElement serializeConfiguration(SynapseConfiguration synCfg) {
        if (synCfg.getDefaultQName() == null) {
            return serializeConfiguration(synCfg, XMLConfigConstants.DEFINITIONS_ELT);
        } else {
            return serializeConfiguration(synCfg, synCfg.getDefaultQName());
        }
    }

    /**
     * This method will serialize the config using the supplied QName
     * (looking up the right class to do it)
     *
     * @param synCfg
     * @param qName
     * @throws XMLStreamException
     */
    public static OMElement serializeConfiguration(SynapseConfiguration synCfg, QName qName) {

        Class cls = (Class) serializerMap.get(qName);
        if (cls == null) {
            String msg = "Unknown Configuration type " +
                    "referenced by configuration element : " + qName;
            log.error(msg);
            throw new SynapseException(msg);
        }

        try {
            ConfigurationSerializer cs = (ConfigurationSerializer) cls.newInstance();
            return cs.serializeConfiguration(synCfg);

        } catch (InstantiationException e) {
            String msg = "Error initializing configuration factory : " + cls;
            log.error(msg);
            throw new SynapseException(msg, e);

        } catch (IllegalAccessException e) {
            String msg = "Error initializing configuration factory : " + cls;
            log.error(msg);
            throw new SynapseException(msg, e);
        }
    }

    /**
     * This method exposes all the ConfigurationFactories and its Extensions
     *
     * @return Map of factories
     */
    public Map getFactoryMap() {
        return factoryMap;
    }

    /**
     * This method exposes all the ConfigurationSerializer and its Extensions
     *
     * @return Map of serializers
     */
    public static Map getSerializerMap() {
        return serializerMap;
    }

    /**
     * Allow the mediator factory finder to act as an XMLToObjectMapper for Mediators
     * (i.e. Sequence Mediator) loaded dynamically from a Registry
     *
     * @param om configuration from which the object is built
     * @param properties bag of properties to pass in any information to the factory
     * @return built object
     */
    @Override
    public Object getObjectFromOMNode(OMNode om, Properties properties) {
        if (om instanceof OMElement) {
            return getConfiguration((OMElement) om, properties);
        } else {
            handleException("Invalid configuration XML : " + om);
        }
        return null;
    }
}
