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

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.util.UIDGenerator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.SynapseException;
import org.apache.synapse.config.XMLToObjectMapper;
import org.apache.synapse.config.xml.XMLConfigConstants;
import org.apache.synapse.config.xml.MediatorPropertyFactory;
import org.apache.synapse.endpoints.Endpoint;
import org.apache.synapse.endpoints.IndirectEndpoint;
import org.apache.synapse.endpoints.EndpointDefinition;
import org.apache.synapse.PropertyInclude;
import org.apache.synapse.mediators.MediatorProperty;
import org.apache.synapse.endpoints.AbstractEndpoint;

import javax.xml.namespace.QName;
import java.util.*;

/**
 * All endpoint factories should extend from this abstract class. Use EndpointFactory to obtain the
 * correct endpoint for particular endpoint configuration. As endpoints can be nested inside
 * each other, EndpointFactory implementations may call other EndpointFactory implementations
 * recursively to obtain the required endpoint hierarchy.
 * <p/>
 * This also serves as the {@link XMLToObjectMapper} implementation for specific endpoint
 * implementations. If the endpoint type is not known use {@link XMLToEndpointMapper} as the
 * generic {@link XMLToObjectMapper} for all endpoints.
 */
public abstract class EndpointFactory implements XMLToObjectMapper {

    static Log log;
    private DefinitionFactory customDefnFactory = null;

    protected EndpointFactory() {
        log = LogFactory.getLog(this.getClass());
    }

    private static final String ENDPOINT_NAME_PREFIX = "endpoint_";

    public static final QName ON_FAULT_Q = new QName(XMLConfigConstants.NULL_NAMESPACE, "onError");

    private static final QName DESCRIPTION_Q
            = new QName(XMLConfigConstants.SYNAPSE_NAMESPACE, "description");

    /**
     * Core method which is exposed for the external use, and this will find the proper
     * {@link EndpointFactory} and create the endpoint which is of the format {@link Endpoint}.
     *
     * @param elem        XML from which the endpoint will be built
     * @param isAnonymous whether this is an anonymous endpoint or not
     * @param properties bag of properties to pass in any information to the factory
     * @return created endpoint
     */
    public static Endpoint getEndpointFromElement(OMElement elem, boolean isAnonymous,
                                                  Properties properties) {
        return getEndpointFactory(elem).createEndpointWithName(elem, isAnonymous, properties);
    }

    /**
     * Core method which is exposed for the external use, and this will find the proper
     * {@link EndpointFactory} and create the endpoint which is of the format {@link Endpoint}.However
     * definition for this endpoint will be built using a custom Endpoint Defn factory.
     *
     * @param elem        XML from which the endpoint will be built
     * @param factory    custom definition factory which this endpoint will be used to build
     * @param isAnonymous whether this is an anonymous endpoint or not
     * @param properties bag of properties to pass in any information to the factory
     * @return created endpoint
     */
    public static Endpoint getEndpointFromElement(OMElement elem,DefinitionFactory factory,
                                                  boolean isAnonymous,
                                                  Properties properties) {
        EndpointFactory fac = getEndpointFactory(elem);
        fac.setEndpointDefinitionFactory(factory);
        return fac.createEndpointWithName(elem, isAnonymous, properties);
    }

    /**
     * Creates the {@link Endpoint} object from the provided {@link OMNode}
     *
     * @param om XML node from which the endpoint will be built
     * @param properties bag of properties to pass in any information to the factory
     * @return created endpoint as an {@link Object}
     */
    @Override
    public Object getObjectFromOMNode(OMNode om, Properties properties) {
        if (om instanceof OMElement) {
            return createEndpointWithName((OMElement) om, false, properties);
        } else {
            handleException("Invalid XML configuration for an Endpoint. OMElement expected");
        }
        return null;
    }

    /**
     * Creates the Endpoint implementation for the given XML endpoint configuration. If the endpoint
     * configuration is an inline one, it should be an anonymous endpoint. If it is defined as an
     * immediate child element of the definitions tag it should have a name, which is used as the
     * key in local registry.
     *
     * @param epConfig          OMElement containing the endpoint configuration.
     * @param anonymousEndpoint false if the endpoint has a name. true otherwise.
     * @param properties bag of properties to pass in any information to the factory
     * @return Endpoint implementation for the given configuration.
     */
    protected abstract Endpoint createEndpoint(OMElement epConfig, boolean anonymousEndpoint,
                                               Properties properties);

    /**
     *  Make sure that the endpoints created by the factory has a name
     * 
     * @param epConfig          OMElement containing the endpoint configuration.
     * @param anonymousEndpoint false if the endpoint has a name. true otherwise.
     * @param properties bag of properties to pass in any information to the factory
     * @return Endpoint implementation for the given configuration.
     */
    private Endpoint createEndpointWithName(OMElement epConfig, boolean anonymousEndpoint,
                                            Properties properties) {
        
        Endpoint ep = createEndpoint(epConfig, anonymousEndpoint, properties);
        OMElement descriptionElem = epConfig.getFirstChildWithName(DESCRIPTION_Q);
        if (descriptionElem != null) {
            ep.setDescription(descriptionElem.getText());
        }

        // if the endpoint doesn't have a name we will generate a unique name.
        if (anonymousEndpoint && ep.getName() == null) {
            String uuid = UIDGenerator.generateUID();
            uuid = uuid.replace(':', '_');
            ep.setName(ENDPOINT_NAME_PREFIX + uuid);
            if (ep instanceof AbstractEndpoint) {
                ((AbstractEndpoint) ep).setAnonymous(true);
            }
        }

        OMAttribute onFaultAtt = epConfig.getAttribute(ON_FAULT_Q);
        if (onFaultAtt != null) {
            ep.setErrorHandler(onFaultAtt.getAttributeValue());
        }
        return ep;
    }

    protected void extractSpecificEndpointProperties(EndpointDefinition definition,
        OMElement elem) {

        // overridden by the Factories which has specific building
    }

    /**
     * Returns the EndpointFactory implementation for given endpoint configuration. Throws a
     * SynapseException, if there is no EndpointFactory for given configuration.
     *
     * @param configElement Endpoint configuration.
     * @return EndpointFactory implementation.
     */
    private static EndpointFactory getEndpointFactory(OMElement configElement) {

        if (configElement.getAttribute(new QName("key")) != null) {
            return IndirectEndpointFactory.getInstance();
        }

        if (configElement.getAttribute(new QName("key-expression")) != null) {
            return ResolvingEndpointFactory.getInstance();
        }

        if (configElement.getAttribute(new QName("template")) != null) {
            return new TemplateEndpointFactory();
        }

        OMElement addressElement = configElement.getFirstChildWithName(
                new QName(SynapseConstants.SYNAPSE_NAMESPACE, "address"));
        if (addressElement != null) {
            return AddressEndpointFactory.getInstance();
        }

        OMElement wsdlElement = configElement.getFirstChildWithName(
                new QName(SynapseConstants.SYNAPSE_NAMESPACE, "wsdl"));
        if (wsdlElement != null) {
            return WSDLEndpointFactory.getInstance();
        }

        OMElement defaultElement = configElement.getFirstChildWithName(
                new QName(SynapseConstants.SYNAPSE_NAMESPACE, "default"));
        if (defaultElement != null) {
            return DefaultEndpointFactory.getInstance();
        }

        OMElement lbElement = configElement.getFirstChildWithName
                (new QName(SynapseConstants.SYNAPSE_NAMESPACE, "loadbalance"));
        if (lbElement != null) {
            OMElement sessionElement = configElement.
                    getFirstChildWithName(new QName(SynapseConstants.SYNAPSE_NAMESPACE, "session"));
            if (sessionElement != null) {
                return SALoadbalanceEndpointFactory.getInstance();
            } else {
                return LoadbalanceEndpointFactory.getInstance();
            }
        }

        OMElement dlbElement = configElement.getFirstChildWithName
                (new QName(SynapseConstants.SYNAPSE_NAMESPACE, "dynamicLoadbalance"));
        if (dlbElement != null) {
            //TODO: Handle Session affinity & failover
            return DynamicLoadbalanceEndpointFactory.getInstance();
        }

        OMElement sdlbElement = configElement.getFirstChildWithName
                (new QName(SynapseConstants.SYNAPSE_NAMESPACE, "serviceDynamicLoadbalance"));
        if (sdlbElement != null) {
            return ServiceDynamicLoadbalanceEndpointFactory.getInstance();
        }
        
        OMElement foElement = configElement.getFirstChildWithName
                (new QName(SynapseConstants.SYNAPSE_NAMESPACE, "failover"));
        if (foElement != null) {
            return FailoverEndpointFactory.getInstance();
        }
        
        OMElement rcplElement = configElement.getFirstChildWithName
        		(new QName(SynapseConstants.SYNAPSE_NAMESPACE, "recipientlist"));
        if(rcplElement != null){
        	return RecipientListEndpointFactory.getInstance();
        }

        OMElement classElement = configElement.getFirstChildWithName
                (new QName(SynapseConstants.SYNAPSE_NAMESPACE, "class"));
        if (classElement != null) {
            return ClassEndpointFactory.getInstance();
        }

        handleException("Invalid endpoint configuration.");
        // just to make the compiler happy : never executes
        return null;
    }

    /**
     * Helper method to construct children endpoints
     *
     * @param listEndpointElement OMElement representing  the children endpoints
     * @param parent Parent endpoint
     * @param properties bag of properties to pass in any information to the factory
     * @return List of children endpoints
     */
    protected ArrayList<Endpoint> getEndpoints(OMElement listEndpointElement, Endpoint parent,
                                               Properties properties) {

        ArrayList<Endpoint> endpoints = new ArrayList<Endpoint>();
        ArrayList<String> keys = new ArrayList<String>();
        Iterator iter = listEndpointElement.getChildrenWithName(XMLConfigConstants.ENDPOINT_ELT);
        while (iter.hasNext()) {
            OMElement endptElem = (OMElement) iter.next();
            Endpoint endpoint = EndpointFactory.getEndpointFromElement(endptElem, true, properties);
            if (endpoint instanceof IndirectEndpoint) {
                String key = ((IndirectEndpoint) endpoint).getKey();
                if (!keys.contains(key)) {
                    keys.add(key);
                } else {
                    handleException("Same endpoint definition cannot be used with in the siblings");
                }
            }
            endpoint.setParentEndpoint(parent);
            endpoints.add(endpoint);
        }

        return endpoints;
    }

    /**
     * provide a custom Endpoint definition factory
     * @param factory
     */
    public void setEndpointDefinitionFactory(DefinitionFactory factory){
        customDefnFactory = factory;
    }

    /**
     * return current factory for building this endpoint definition
     * @return
     */
    public DefinitionFactory getEndpointDefinitionFactory(){
        return customDefnFactory;
    }

    /**
     * Helper method to extract endpoint properties.
     *
     * @param endpoint actual endpoint to set the properties
     * @param endpointElement actual endpoint element
     */
    protected void processProperties(PropertyInclude endpoint, OMElement endpointElement) {
        List<MediatorProperty> properties =
                MediatorPropertyFactory.getMediatorProperties(endpointElement);

        if (properties != null && properties.size() > 0) {
            endpoint.addProperties(properties);
        }
    }

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

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