/*
 *  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.util.xpath.ext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseException;
import org.apache.synapse.config.SynapsePropertiesLoader;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.core.axis2.Axis2SynapseEnvironment;
import org.jaxen.Function;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.xml.namespace.QName;

/**
 * Utility class to support custom xpath context extensions
 */
public class XpathExtensionUtil {
    /**
     * The Synapse extension property for variable context extensions
     */
    private static final String SYNAPSE_XPATH_VARIABLE_EXTENSIONS = "synapse.xpath.var.extensions";

    /**
     * The Synapse extension property for function context extensions
     */
    private static final String SYNAPSE_XPATH_FUNCTION_EXTENSIONS = "synapse.xpath.func.extensions";

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

    /**
     * This method initializes Xpath Extensions available through synapse.properties file.
     * Xpath Extensions can be defined in Variable Context Extensions + Function Context Extensions
     * synapse.xpath.var.extensions --> Variable Extensions
     * synapse.xpath.func.extensions --> Function Extensions
     *
     * @param synapseEnvironment SynapseEnvironment
     */
    public static void initXpathExtensions(SynapseEnvironment synapseEnvironment) {
        Axis2SynapseEnvironment axis2SynapseEnvironment = (Axis2SynapseEnvironment) synapseEnvironment;

        /*Initialize Function Context extensions for xpath
         */
        List<SynapseXpathFunctionContextProvider> functionExtensions = getRegisteredFunctionExtensions();
        for (SynapseXpathFunctionContextProvider functionExtension : functionExtensions) {
            axis2SynapseEnvironment.setXpathFunctionExtensions(functionExtension);
        }

        /*Initialize Variable Context extensions for xpath
         */
        List<SynapseXpathVariableResolver> variableExtensions = getRegisteredVariableExtensions();
        for (SynapseXpathVariableResolver variableExtension : variableExtensions) {
            axis2SynapseEnvironment.setXpathVariableExtensions(variableExtension);
        }
    }

    /**
     * Get all registered variable context extensions. Synapse will look for synapse.properties
     * property synapse.xpath.var.extensions
     *
     * @return List of Synapse Xpath Variable Context Providers
     */
    public static List<SynapseXpathVariableResolver> getRegisteredVariableExtensions() {
        Properties synapseProps = SynapsePropertiesLoader.loadSynapseProperties();
        String propValue = synapseProps.getProperty(SYNAPSE_XPATH_VARIABLE_EXTENSIONS);
        List<SynapseXpathVariableResolver> extProviders = new
                ArrayList<SynapseXpathVariableResolver>();
        extractProviders(propValue, extProviders);
        return extProviders;
    }

    /**
     * Get all registered function context extensions. Synapse will look for synapse.properties
     * property synapse.xpath.func.extensions
     *
     * @return List of Synapse Xpath Function Context Providers
     */
    public static List<SynapseXpathFunctionContextProvider> getRegisteredFunctionExtensions() {
        Properties synapseProps = SynapsePropertiesLoader.loadSynapseProperties();
        String propValue = synapseProps.getProperty(SYNAPSE_XPATH_FUNCTION_EXTENSIONS);
        List<SynapseXpathFunctionContextProvider> extProviders = new
                ArrayList<SynapseXpathFunctionContextProvider>();
        extractProviders(propValue, extProviders);
        return extProviders;
    }

    /**
     * Populate the set of registered  Extension proiders for a given property
     *
     * @param propValue    either variable/function provider property name
     * @param extProviders a List that will be populated with the registered extensions
     * @param <T>          Variable/Function Context Provider Type
     */
    private static <T> void extractProviders(String propValue, List<T> extProviders) {
        if (propValue != null) {
            String[] observerNames = propValue.split(",");
            for (String observer : observerNames) {
                try {
                    Class clazz = XpathExtensionUtil.class.getClassLoader().
                            loadClass(observer.trim());
                    T obj = (T) clazz.newInstance();
                    extProviders.add(obj);
                } catch (Exception e) {
                    handleException("Error while initializing Synapse Xpath extension providers", e);
                }
            }
        }
    }

    /**
     * Returns a Function Context extension registered for given QName/namespaceURI+prefix+localName
     * combination
     *
     * @param ctxt         Synapse Message Context
     * @param namespaceURI binding namespace in xpath expression
     * @param prefix       binding prefix string in xpath expression
     * @param localName    binding localname string in xpath expression
     * @return jaxen Function object for corresponding extension
     */
    public static Function getFunctionContext(MessageContext ctxt, String namespaceURI, String prefix,
                                              String localName) {
        SynapseEnvironment environment = ctxt.getEnvironment();
        if (environment != null) {
            Map<String, SynapseXpathFunctionContextProvider> extensions =
                    environment.getXpathFunctionExtensions();
            SynapseXpathFunctionContextProvider functionContextProvider =
                    getMatchingExtensionProvider(extensions, prefix, localName);
            if (functionContextProvider != null) {
                return initAndReturnXpathFunction(functionContextProvider, ctxt);
            }
        }
        return null;
    }

    /**
     * Returns an object resolved by Variable Context extension registered for given
     * QName/namespaceURI+prefix+localName combination
     *
     * @param ctxt         Synapse Message Context
     * @param namespaceURI binding namespace in xpath expression
     * @param prefix       binding prefix string in xpath expression
     * @param localName    binding localname string in xpath expression
     * @return Object variable resolved by corresponding extension
     */
    public static Object resolveVariableContext(MessageContext ctxt, String namespaceURI,
                                                String prefix, String localName) {
        SynapseEnvironment environment = ctxt.getEnvironment();
        if (environment != null) {
            Map<QName, SynapseXpathVariableResolver> extensions =
                    environment.getXpathVariableExtensions();
            SynapseXpathVariableResolver variableResolver =
                    getMatchingExtensionProvider(extensions, namespaceURI, prefix, localName);
            if (variableResolver != null) {
                return resolveXpathVariable(variableResolver, ctxt);
            }
        }
        return null;
    }

    /**
     * returns the matching Extension provider for a given QName/namespaceURI+prefix+localName
     * combination
     *
     * @param extensionMap registered extension Map for the corresponding extension provider
     * @param namespaceURI binding namespace in xpath expression
     * @param prefix       binding prefix string in xpath expression
     * @param localName    binding localname string in xpath expression
     * @param <T>          Variable/Function Context Provider Type
     * @return matching Extension provider. returns null if no extension is found for the given
     *         combination
     */
    private static <T> T getMatchingExtensionProvider(Map<QName, T> extensionMap,
                                                      String namespaceURI,
                                                      String prefix, String localName) {
        QName subject;
        if (localName != null && prefix != null) {
            subject = new QName(namespaceURI, localName, prefix);
        } else if (localName != null) {
            subject = new QName(namespaceURI, localName);
        } else {
            //can't resolve xpath extensions - invalid combination
            return null;
        }

        Set<QName> qNames = extensionMap.keySet();
        for (QName qName : qNames) {
            //check for a match for the given combination for QName registered
            if (subject.equals(qName)) {
                return extensionMap.get(qName);
            }
        }
        //no match found
        return null;

    }

    /**
     * returns the matching Extension provider for a given prefix+localName
     * combination
     *
     * @param extensionMap registered extension Map for the corresponding extension provider
     * @param prefix       binding prefix string in xpath expression
     * @param localName    binding localname string in xpath expression
     * @return matching Extension provider. returns null if no extension is found for the given
     * combination
     */
    private static SynapseXpathFunctionContextProvider getMatchingExtensionProvider(
            Map<String, SynapseXpathFunctionContextProvider> extensionMap, String prefix, String localName) {
        String subject;
        if (localName != null && prefix != null) {
            subject = prefix + localName;
        } else if (localName != null) {
            subject = localName;
        } else {
            //can't resolve xpath extensions - invalid combination
            return null;
        }
        Set<String> qNames = extensionMap.keySet();
        for (String qName : qNames) {
            //check for a match for the given combination for QName registered
            if (subject.equals(qName)) {
                return extensionMap.get(qName);
            }
        }
        //no match found
        return null;
    }

    /**
     * try to resolve an xpath variable context using the extension given
     *
     * @param variableExt Extension provider for variable contexts
     * @param ctxt        Synapse Message Context
     * @return resolved property/object
     */
    private static Object resolveXpathVariable(SynapseXpathVariableResolver variableExt,
                                               MessageContext ctxt) {
        try {
            return variableExt.resolve(ctxt);
        } catch (Exception e) {
            handleExceptionWarning("Error Invoking Xpath Variable Provider " +
                                   variableExt.getClass().getName(), e);
        }
        return null;
    }

    /**
     * Initializes an returns new Xpath Function
     *
     * @param funcExtProvider extension provider for a Function Context
     * @param ctxt            Synapse Message Context
     * @return Xpath Function instance . returns null if error occurs
     */
    private static Function initAndReturnXpathFunction(
            SynapseXpathFunctionContextProvider funcExtProvider,
            MessageContext ctxt) {
        try {
            return funcExtProvider.getInitializedExtFunction(ctxt);
        } catch (Exception e) {
            handleExceptionWarning("Error Initializing Xpath Function Provider " +
                                   funcExtProvider.getClass().getName(), e);
        }
        return null;
    }

    private static void handleException(String msg, Exception e) {
        log.warn(msg, e);
        throw new SynapseException(msg, e);
    }

    private static void handleExceptionWarning(String msg, Exception e) {
        log.warn(msg, e);
    }
}
