/**
 * Copyright (c) 2015-2024 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.binding.soap.util;

import java.io.StringReader;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

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

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.AddressingConstants;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.deployment.DeploymentConstants;
import org.apache.axis2.deployment.DeploymentErrorMsgs;
import org.apache.axis2.deployment.DeploymentException;
import org.apache.axis2.description.AxisModule;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.InOutAxisOperation;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.transport.jms.JMSConstants;
import org.apache.axis2.util.XMLUtils;
import org.apache.neethi.Policy;
import org.ow2.easywsdl.extensions.wsdl4complexwsdl.api.Description;
import org.ow2.easywsdl.schema.api.Element;
import org.ow2.easywsdl.wsdl.api.Binding;
import org.ow2.easywsdl.wsdl.api.BindingOperation;
import org.ow2.easywsdl.wsdl.api.Input;
import org.ow2.easywsdl.wsdl.api.Operation;
import org.ow2.easywsdl.wsdl.api.Part;
import org.ow2.easywsdl.wsdl.api.abstractItf.AbsItfDescription.WSDLVersionConstants;
import org.ow2.easywsdl.wsdl.api.binding.BindingProtocol.SOAPMEPConstants;
import org.ow2.petals.binding.soap.ServiceContext;
import org.ow2.petals.binding.soap.SoapComponentContext;
import org.ow2.petals.binding.soap.SoapConstants;
import org.ow2.petals.binding.soap.SoapSUManager;
import org.ow2.petals.binding.soap.listener.incoming.PetalsLoadOperationsFromWSDLDispatcher;
import org.ow2.petals.binding.soap.listener.incoming.SoapServerConfig;
import org.ow2.petals.commons.log.Level;
import org.ow2.petals.component.framework.api.configuration.ConfigurationExtensions;
import org.ow2.petals.component.framework.api.configuration.SuConfigurationParameters;
import org.ow2.petals.component.framework.api.exception.PEtALSCDKException;
import org.ow2.petals.component.framework.jbidescriptor.generated.Consumes;

import com.ebmwebsourcing.easycommons.lang.StringHelper;

/**
 * 
 * @author vnoel
 *
 */
public class AxisServicesHelper {

    private AxisServicesHelper() {

    }

    /**
     * Creates the OMElement corresponding to the parameters String, included in parameters tags.
     * 
     * @param parameters
     *            the parameters
     * @return Returns the service parameters node
     * @throws XMLStreamException
     */
    private static OMElement buildParametersOM(final String serviceParams) throws XMLStreamException {
        if (serviceParams != null) {
            return (OMElement) XMLUtils.toOM(new StringReader("<parameters>" + serviceParams + "</parameters>"));
        } else {
            return null;
        }
    }

    /**
     * Add the service parameters to the specified Axis 2 service
     * 
     * @param serviceParams
     *            Service parameters (in Axis2 format, as into axis2.xml) to add
     * @param axisService
     *            the Axis 2 service
     * @throws XMLStreamException
     * @throws AxisFault
     */
    public static void addServiceParameters(final String serviceParams, final AxisService axisService)
            throws XMLStreamException, AxisFault {

        final OMElement parametersElements = buildParametersOM(serviceParams);

        if (parametersElements != null) {
            // get an iterator on all <parameter> children
            @SuppressWarnings("unchecked")
            final Iterator<OMElement> itr = parametersElements
                    .getChildrenWithName(new QName(DeploymentConstants.TAG_PARAMETER));

            // iterate on parameters and set them to the associated
            // axisService
            while (itr.hasNext()) {
                final OMElement parameterElement = itr.next();

                if (DeploymentConstants.TAG_PARAMETER.equalsIgnoreCase(parameterElement.getLocalName())) {
                    axisService.addParameter(getParameter(parameterElement));
                }
            }
        }
    }

    /**
     * Process the parameterElement object from the OM, and returns the corresponding Parameter.
     * 
     * @param parameterElement
     *            <code>OMElement</code>
     * @return the Parameter parsed
     * @throws DeploymentException
     *             if bad paramName
     */
    private static Parameter getParameter(final OMElement parameterElement) throws DeploymentException {
        final Parameter parameter = new Parameter();

        // setting parameterElement
        parameter.setParameterElement(parameterElement);

        // setting parameter Name
        final OMAttribute paramName = parameterElement.getAttribute(new QName(DeploymentConstants.ATTRIBUTE_NAME));

        if (paramName == null) {
            throw new DeploymentException(
                    Messages.getMessage(DeploymentErrorMsgs.BAD_PARAMETER_ARGUMENT, parameterElement.toString()));
        }
        parameter.setName(paramName.getAttributeValue());

        // setting parameter Value (the child element of the parameter)
        final OMElement paramValue = parameterElement.getFirstElement();
        if (paramValue != null) {
            parameter.setValue(paramValue);
            parameter.setParameterType(Parameter.OM_PARAMETER);
        } else {
            final String paratextValue = parameterElement.getText();
            parameter.setValue(paratextValue);
            parameter.setParameterType(Parameter.TEXT_PARAMETER);
        }

        return parameter;
    }

    /**
     * Set an Axis service as a SOAP over HTTPS service.
     * 
     * @param service
     */
    private static void setTransportHttpsToAxisService(final AxisService service,
            final ConfigurationExtensions componentExtensions, final SuConfigurationParameters suExtensions,
            final SoapServerConfig soapServerConfig, final Logger logger) {

        if (ComponentPropertiesHelper.isHttpsEnabled(componentExtensions)
                && SUPropertiesHelper.isHttpsTransportEnabled(suExtensions)) {
            service.addExposedTransport(Constants.TRANSPORT_HTTPS);

            logger.log(Level.INFO,
                    "The Axis2 service '" + service.getName() + "' has been registered and is available at '"
                            + soapServerConfig.getServiceURL(service.getName(), Constants.TRANSPORT_HTTPS) + "'");
        }
    }

    /**
     * Set an Axis service as a SOAP over HTTP service.
     * 
     * @param service
     */
    private static void setTransportHttpToAxisService(final AxisService service, final SuConfigurationParameters extensions,
            final SoapServerConfig soapServerConfig, final Logger logger) {

        if (SUPropertiesHelper.isHttpTransportEnabled(extensions)) {
            service.addExposedTransport(Constants.TRANSPORT_HTTP);

            logger.log(Level.INFO,
                    "The Axis2 service '" + service.getName() + "' has been registered and is available at '"
                            + soapServerConfig.getServiceURL(service.getName(), Constants.TRANSPORT_HTTP) + "'");
        }
    }
    

    /**
     * Set an Axis service as a SOAP over JMS service.
     * 
     * @param axisService
     * @throws AxisFault
     */
    private static void setTransportJmsToAxisService(final AxisService axisService,
            final SuConfigurationParameters extensions, final Logger logger) throws AxisFault {

        if (SUPropertiesHelper.isJmsTransportEnabled(extensions)) {

            axisService.addParameter(JMSConstants.PARAM_DESTINATION, axisService.getName());
            axisService.addExposedTransport(Constants.TRANSPORT_JMS);

            logger.log(Level.INFO, "The Axis2 service '" + axisService.getName()
                    + "' has been registered and is available through JMS.");
        }
    }

    public static void registerAxisService(final String serviceName, final ServiceContext<Consumes> context,
            final SoapComponentContext soapContext) throws AxisFault, PEtALSCDKException {

        final AxisService service = new AxisService(serviceName);

        final Logger logger = context.getLogger();

        service.setClassLoader(context.getClassloader());

        service.addParameter(SoapConstants.Axis2.CONSUMES_SERVICE_CONTEXT_PARAM, context);
        service.addParameter(Constants.Configuration.SEND_STACKTRACE_DETAILS_WITH_FAULTS,
                Boolean.toString(soapContext.isPublicStacktracesEnabled()));

        final Consumes consumes = context.getConfig();
        final SuConfigurationParameters consumesExtensions = context.getExtensions();

        // populate service with service descriptor
        final QName jbiServiceQName = consumes.getServiceName();
        if (jbiServiceQName != null) {
            service.setTargetNamespace(jbiServiceQName.getNamespaceURI());
        }

        // Either the WSDL is already available (because it is taken from the consumes or endpoint provided by the bus)
        // or if it is not, it will be loaded on demand
        try {
            // Note: if PEtALSCDKException is thrown, it shouldn't be catched!
            addAxisOperationsFromWSDL(service, context);
        } catch (final AxisFault e) {
            logger.info(
                    "Can't populate AxisService operations from the WSDL for now: it will be done on-demand on the first call to the service.");
            service.addParameter(SoapConstants.Axis2.LOAD_OPERATIONS_FROM_WSDL_ON_DEMAND, Boolean.TRUE.toString());
        }

        // set the transport layers
        // (necessary to set the transport before adding the Axis service to Axis configuration)
        service.setEnableAllTransports(false);
        setTransportHttpsToAxisService(service, soapContext.getComponentConfigurationExtensions(), consumesExtensions,
                soapContext.getSoapServerConfig(), logger);
        setTransportHttpToAxisService(service, consumesExtensions, soapContext.getSoapServerConfig(), logger);
        setTransportJmsToAxisService(service, consumesExtensions, logger);

        final AxisConfiguration axisConfig = soapContext.getAxis2ConfigurationContext().getAxisConfiguration();

        // Add the service to the config (needed for engaging a module)
        axisConfig.addService(service);

        // disable WS-Addressing if necessary
        if (!SUPropertiesHelper.isWSAEnabled(consumesExtensions)) {
            // Note: we can't engage addressing at the service level, it can only be global (axis2 restriction...)
            service.addParameter(AddressingConstants.DISABLE_ADDRESSING_FOR_IN_MESSAGES, Boolean.TRUE.toString());
        }

        // enable WS-Security if necessary
        if (SUPropertiesHelper.getModules(consumesExtensions).contains(SoapConstants.Axis2.RAMPART_MODULE)) {
            // TODO there is bug here, we can't solve it for now, see PETALSBCSOAP-160
            final AxisModule axisModule = axisConfig.getModule(SoapConstants.Axis2.RAMPART_MODULE);
            service.engageModule(axisModule);
            final Policy wssPolicy = SUPropertiesHelper.getWSSPolicy(consumesExtensions, context.getClassloader(),
                    logger);
            if (wssPolicy != null) {
                service.getPolicySubject().attachPolicy(wssPolicy);
            }
        }

        // Set the associated service parameters provided as CDATA from the extension 'service-parameters'.
        try {
            AxisServicesHelper.addServiceParameters(SUPropertiesHelper.getServiceParameters(consumesExtensions),
                    service);
        } catch (final XMLStreamException | AxisFault e) {
            logger.log(Level.WARNING, "Can't add service parameters", e);
        }
    }

    private static void addAxisOperationsFromWSDL(final AxisService service, final ServiceContext<Consumes> context)
            throws AxisFault, PEtALSCDKException {

        final SuConfigurationParameters consumesExtensions = context.getExtensions();

        // we need to be sure the description we have is from the jbi and not generated by the CDK
        final String wsdlFile = consumesExtensions.get(SoapConstants.ServiceUnit.WSDL_FILE);

        final Description descOrNull;
        if (!StringHelper.isNullOrEmpty(wsdlFile)) {
            descOrNull = context.getServiceDescription();
        } else {
            descOrNull = null;
        }

        addAxisOperationsFromWSDL(service, descOrNull, context);

    }

    public static void addAxisOperationsFromWSDL(final AxisService service) throws AxisFault {
        @SuppressWarnings("unchecked")
        final ServiceContext<Consumes> serviceContext = (ServiceContext<Consumes>) service
                .getParameter(SoapConstants.Axis2.CONSUMES_SERVICE_CONTEXT_PARAM).getValue();

        addAxisOperationsFromWSDL(service, null, serviceContext);
    }

    /**
     * This is needed because Axis needs to know about which operations are available in order to correctly setup the
     * {@link MessageContext} when a request arrives on the SOAP side.
     * 
     * It is first called when a Consumes is started (in {@link SoapSUManager}) and then if there is no WSDL defined or
     * if the provider (which gives use the WSDL) is not there yet, it will be called in
     * {@link PetalsLoadOperationsFromWSDLDispatcher} on message arrival.
     * 
     */
    private static void addAxisOperationsFromWSDL(final AxisService service, final Description descOrNull,
            final ServiceContext<Consumes> context) throws AxisFault {

        final Consumes consumes = context.getConfig();
        final SoapComponentContext soapContext = context.getComponentContext();

        final Description desc;
        if (descOrNull == null) {
            // this never returns null
            desc = WsdlHelper.getDescription(consumes, soapContext.getComponent(), context.getLogger());
        } else {
            desc = descOrNull;
        }

        for (final Binding binding : desc.getBindings()) {
            for (final BindingOperation bindingOperation : binding.getBindingOperations()) {
                final Operation operation = bindingOperation.getOperation();
                // We use this as a generic operation: anyway Axis will directly call our code through the
                // PetalsReceiver
                final AxisOperation genericOperation = new InOutAxisOperation(operation.getQName());
                genericOperation.setMessageReceiver(soapContext.getPetalsReceiver());

                // we need these things so that the various Dispatcher can find the operation
                genericOperation.setSoapAction(bindingOperation.getSoapAction());
                genericOperation.setMessageExchangePattern(getWSDL2Mep(bindingOperation));

                service.addOperation(genericOperation);

                // this is needed so that org.apache.axis2.dispatchers.SOAPMessageBodyBasedDispatcher
                // can find the right operation based on the element
                final Input input = operation.getInput();
                if (input != null) {
                    if (desc.getVersion().equals(WSDLVersionConstants.WSDL11)) {
                        // TODO theoretically, there should be in the binding parts that are referenced... or if none,
                        // all should be used... but here we directly use the message's parts without looking at the
                        // binding...
                        final List<Part> parts = input.getParts();
                        if (!parts.isEmpty()) {
                            // if there is more than one part, it's not so valid but well...
                            // TODO we should handle differently if it's wrapped style or unwrapped style
                            // let's hope that it's the correct one that is in the message first...
                            final Element element = parts.get(0).getElement();
                            if (element != null) {
                                final QName firstElement = element.getQName();
                                service.addMessageElementQNameToOperationMapping(firstElement, genericOperation);
                                if (service.getOperationByMessageElementQName(firstElement) == null) {
                                    // see how addMessageElementQNameToOperationMapping behave
                                    context.getLogger()
                                            .warning("There seems to be more than one operation using the element "
                                                    + firstElement
                                                    + ". If there is no other way than using the SOAP message content to determine the operation (except for matching element name to operation name), then there will be an unsolvable ambiguity.");
                                }
                                if (parts.size() > 1) {
                                    context.getLogger().warning("More than one part found for input "
                                            + input.getName()
                                            + "(operation " + operation.getQName()
                                            + "). If a received message does not start with the first WSDL declared part and there is no other way than using the SOAP message content to determine the operation (except for matching element name to operation name), then there will be an unsolvable ambiguity.");
                                }
                            } else {
                                context.getLogger().warning("An element referenced by the input "
                                        + input.getName()
                                        + "(operation " + operation.getQName()
                                        + ") seems to be missing from the types present in the WSDL... skipping this input! If there is no other way than using the SOAP message content to determine the operation (except for matching element name to operation name), then there will be an unsolvable ambiguity.");
                            }
                        } else {
                            // no parts means empty message, this is ok, axis2 will understand
                            service.addMessageElementQNameToOperationMapping(null, genericOperation);
                        }
                    } else if (desc.getVersion().equals(WSDLVersionConstants.WSDL20)) {
                        // for WSDL20, there is no ambiguity
                        service.addMessageElementQNameToOperationMapping(input.getElement().getQName(),
                                genericOperation);
                    } else {
                        context.getLogger().warning("Unknown WSDL version for " + desc.getDocumentBaseURI());
                    }
                }
            }
        }
    }

    private static String getWSDL2Mep(final BindingOperation operation) {
        final SOAPMEPConstants soapMep = operation.getMEP();
        // TODO shoudn't we also look at the WSDL input/output/fault to know the MEP? like robust or in opt out?
        if (soapMep == SOAPMEPConstants.ONE_WAY) {
            if (!operation.getFaults().isEmpty()) {
                return WSDL2Constants.MEP_URI_ROBUST_IN_ONLY;
            } else {
                return WSDL2Constants.MEP_URI_IN_ONLY;
            }
        } else if (soapMep == SOAPMEPConstants.REQUEST_RESPONSE) {
            return WSDL2Constants.MEP_URI_IN_OUT;
        } else {
            return WSDL2Constants.MEP_URI_OUT_ONLY;
        }
    }

    /**
     * Unregister the service from Axis.
     * 
     * @param context
     * @param extensions
     * @throws PEtALSCDKException
     */
    public static void unregisterAxisService(final SoapComponentContext context,
            final SuConfigurationParameters extensions) throws PEtALSCDKException {
        String serviceName = SUPropertiesHelper.getServiceName(extensions);
        if (serviceName == null) {
            serviceName = SUPropertiesHelper.getAddress(extensions);
        }
        final AxisConfiguration axisConfig = context.getAxis2ConfigurationContext().getAxisConfiguration();
        context.getLogger().info("Removing Axis service '" + serviceName + "'");
        try {
            // register an axis service to axis engine
            final AxisService axisService = axisConfig.getService(serviceName);
            // Feature request : #306664
            // TODO : We do not have to remove the service group. It is
            // temporary until the next version of Axis2 where the removeService
            // method will fix it.
            if (axisService != null) {
                axisConfig.removeServiceGroup(serviceName);
                axisConfig.removeService(serviceName);
            } else {
                context.getLogger()
                        .warning("Service '" + serviceName + "' not found, can not be unregistered from Axis2");
            }
        } catch (final AxisFault e) {
            throw new PEtALSCDKException("Can not remove service from Axis context", e);
        }
    }
}
