All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ow2.petals.binding.soap.listener.outgoing.ServiceClientPoolObjectFactory Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2008-2012 EBM WebSourcing, 2012-2023 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.listener.outgoing;

import static org.apache.axis2.addressing.AddressingConstants.DISABLE_ADDRESSING_FOR_OUT_MESSAGES;
import static org.ow2.petals.binding.soap.SoapConstants.Axis2.OUTGOING_SERVICE_CLIENT_PREFIX;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.logging.Level;

import javax.jbi.messaging.MessagingException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.xml.stream.XMLStreamException;

import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.OutInAxisOperation;
import org.apache.axis2.description.OutOnlyAxisOperation;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.axis2.kernel.TransportSender;
import org.apache.axis2.kernel.http.HTTPConstants;
import org.apache.axis2.transport.http.HttpTransportProperties;
import org.apache.axis2.transport.http.impl.httpclient4.HTTPClient4TransportSender;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.http.client.HttpClient;
import org.apache.http.config.Registry;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.neethi.Policy;
import org.ow2.petals.binding.soap.ServiceContext;
import org.ow2.petals.binding.soap.util.AxisServicesHelper;
import org.ow2.petals.binding.soap.util.SUPropertiesHelper;
import org.ow2.petals.component.framework.api.Message.MEPConstants;
import org.ow2.petals.component.framework.api.configuration.SuConfigurationParameters;
import org.ow2.petals.component.framework.jbidescriptor.generated.Provides;

import com.ebmwebsourcing.easycommons.uuid.QualifiedUUIDGenerator;

/**
 * Get new Axis2 ServiceClient from the pool factory.
 * @author Christophe DENEUX - Cap Gemini
 * @author Christophe HAMERLING - EBM WebSourcing
 */
public class ServiceClientPoolObjectFactory extends BasePooledObjectFactory {

    private static final String ORIGINAL_OPTIONS_TO_RESTORE_ON_RETURN = "org.ow2.petals.binding.soap.SoapComponentContext.ORIGINAL_OPTIONS_TO_RESTORE_ON_RETURN";

    private final ServiceClientConfiguration serviceClientConfiguration;

    private final ConfigurationContext configuration;

    private final ServiceContext context;

    private final QualifiedUUIDGenerator uuidGenerator = new QualifiedUUIDGenerator(OUTGOING_SERVICE_CLIENT_PREFIX);

    /**
     * Creates a new instance of ServiceClientPoolObjectFactory
     * 
     * @param serviceClientConfiguration
     * @param serviceContext
     * @param configuration
     */
    public ServiceClientPoolObjectFactory(final ServiceClientConfiguration serviceClientConfiguration,
            final ServiceContext serviceContext, final ConfigurationContext configuration) {
        this.serviceClientConfiguration = serviceClientConfiguration;
        this.configuration = configuration;
        this.context = serviceContext;
    }

    /**
     * Create the SOAP options of the outgoing SOAP message
     * 
     * @param address
     *            the address of the partner service
     * @param soapAction
     *            the SOAP action
     * @param extensions
     *            the SU extension
     * @return the Axis 2 client options
     * @throws MessagingException
     *             if there is an error to set transport
     * @throws IOException
     */
    private Options createOptions() throws MessagingException {

        final Options options = new Options();

        final SuConfigurationParameters extensions = this.context.getExtensions();

        try {
            // set destination address AND check if the address is a valid URI
            // TODO shouldn't it be an URL?
            options.setTo(new EndpointReference(new URI(this.serviceClientConfiguration.address).toString()));
        } catch (final URISyntaxException e) {
            throw new MessagingException(
                    "Invalid external web-service address: '" + this.serviceClientConfiguration.address + "'");
        }
        
        
        // set the content type (necessary for the formatter)
        options.setProperty(org.apache.axis2.Constants.Configuration.MESSAGE_TYPE, HTTPConstants.MEDIA_TYPE_APPLICATION_SOAP_XML);
        
        // disable WSA-Addressing
        if (!SUPropertiesHelper.isWSAEnabled(extensions)) {
            options.setProperty(DISABLE_ADDRESSING_FOR_OUT_MESSAGES, Boolean.TRUE);
        }

        // set the soap action
        if (this.serviceClientConfiguration.soapAction != null) {
            options.setAction(this.serviceClientConfiguration.soapAction);
        }
        
        // Get the soapEnvelopeNamespaceURI version to use - optional,
        // default
        // is 1.1 version
        final String soapEnvelopeNamespaceURI = SUPropertiesHelper.retrieveSOAPEnvelopeNamespaceURI(extensions);
        options.setSoapVersionURI(soapEnvelopeNamespaceURI);

        // create and register transports
        final ConnectionSocketFactory connectionSocketFactory = this.createConnectionSocketFactory();
        final Registry socketFactoryRegistry = this.registerTransport(connectionSocketFactory,
                options);

        // get proxy settings if they are defined in the extensions
        final HttpTransportProperties.ProxyProperties proxyProperties = SUPropertiesHelper
                .retrieveProxySettings(extensions, context.getLogger());
        if (proxyProperties != null) {
            options.setProperty(HTTPConstants.PROXY, proxyProperties);
        }

        // prevent Axis from transforming SOAP fault to Axis fault
        options.setExceptionToBeThrownOnSOAPFault(false);

        SUPropertiesHelper.setBasicAuthentication(extensions, options);

        // note: we need integer and not long there, hence the use of BigDecimal for the conversion (it will use the max
        // it can to fit an int) (we could have used intValueExact but we don't really want to throw an exception
        // here... we will log a warning)
        final int intTimeout = new BigDecimal(this.serviceClientConfiguration.timeout).intValue();
        if (this.serviceClientConfiguration.timeout != (int) this.serviceClientConfiguration.timeout) {
            this.context.getLogger().log(Level.WARNING,
                    "Provides timeout was set to " + this.serviceClientConfiguration.timeout
                            + " but Axis uses int to store value and the long value can't fit in an int, using "
                            + intTimeout + ".");
        }
        
        // These two are used by CommonsHTTPTransportSender (that we use in the transport, see
        // setTransport()) and are used (over the values set in the connection manager that we don't set, because
        // it is useless) by AbstractHTTPSender.setTimeouts()
        options.setProperty(HTTPConstants.CONNECTION_TIMEOUT, intTimeout);
        options.setProperty(HTTPConstants.SO_TIMEOUT, intTimeout);
        
        // this is used by OutInAxisOperationClient to send asynchronously
        options.setTimeOutInMilliSeconds(this.serviceClientConfiguration.timeout);

        // Theoretically (according to Options documentation), one should use HTTPConstants.REUSE_HTTP_CLIENT
        // for it to be used, but in practice it is not! Still, we need to prevent reuse of the HttpState of the
        // HttpClient: this is done per borrowed ServiceClient in SoapComponentContext.borrowServiceClient()
        final HttpClient hc = HttpClientBuilder.create()
                .setConnectionManager(this.createHttpClientConnectionManager(socketFactoryRegistry))
                .build();
        options.setProperty(HTTPConstants.CACHED_HTTP_CLIENT, hc);

        return options;
    }

    private ConnectionSocketFactory createConnectionSocketFactory() throws MessagingException {
        
        final SuConfigurationParameters extensions = this.context.getExtensions();
        // get transport protocol
        final URI uri = URI.create(this.serviceClientConfiguration.address);
        final String scheme = uri.getScheme();
        String transport = null;
        if (scheme == null) {
            transport = Constants.TRANSPORT_HTTP;
        } else {
            transport = scheme;
        }

        if (transport.equals(Constants.TRANSPORT_HTTP)) {
            return null;

        } else if (transport.equals(Constants.TRANSPORT_HTTPS)) {

            final KeyStore keystore;
            final String keystoreFileStr = SUPropertiesHelper.getKeystoreFile(extensions);
            if (keystoreFileStr != null) {
                final File keystoreFile = new File(keystoreFileStr);
                final String keystorePassword = SUPropertiesHelper.getKeystorePassword(extensions);
                
                try (final InputStream ksInputStream = new FileInputStream(keystoreFile)) {
                    keystore = KeyStore.getInstance("JKS");
                    keystore.load(ksInputStream, keystorePassword.toCharArray());
                } catch (final IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
                    throw new MessagingException(String.format("Error reading the keystore '%s'", keystoreFileStr), e);
                }

                if (context.getLogger().isLoggable(Level.INFO)) {
                    context.getLogger().log(Level.INFO, "Client authentication is enabled.");
                }
            } else {
                keystore = null;
            }

            final TrustManager[] trustManagers;
            final String truststoreFileStr = SUPropertiesHelper.getTruststoreFile(extensions);
            if (truststoreFileStr != null) {
                final File truststoreFile = new File(truststoreFileStr);
                final String truststorePassword = SUPropertiesHelper.getTruststorePassword(extensions);
                
                try (final InputStream ksInputStream = new FileInputStream(truststoreFile)) {
                    final KeyStore truststore = KeyStore.getInstance("JKS");
                    truststore.load(ksInputStream, truststorePassword.toCharArray());
                    
                    final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                    trustManagerFactory.init(keystore);
                    trustManagers = trustManagerFactory.getTrustManagers();
                } catch (final IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
                    throw new MessagingException(String.format("Error reading the truststore '%s'", truststoreFileStr),e);
                }
            } else {
                trustManagers = null;
            }
            
            try {
                final SSLContext sslContext = SSLContext.getDefault();
                sslContext.init(null, trustManagers, new SecureRandom());
                return new SSLConnectionSocketFactory(sslContext);
            } catch (final NoSuchAlgorithmException | KeyManagementException e) {
                throw new MessagingException("Error creating SSL context",e);
            }

        } else {
            throw new UnsupportedOperationException(String
                    .format("Transport protocol '%s' (extracted from url '%s') not supported.", transport,
                            this.serviceClientConfiguration.address));
        }
    }

    private Registry registerTransport(final ConnectionSocketFactory connectionSocketFactory,
            final Options options)
            throws MessagingException {
        
        final SuConfigurationParameters extensions = context.getExtensions();
        // get transport protocol
        final URI uri = URI.create(this.serviceClientConfiguration.address);
        final String scheme = uri.getScheme();
        String transport = null;
        if (scheme == null) {
            transport = Constants.TRANSPORT_HTTP;
        } else {
            transport = scheme;
        }

        TransportSender sender;
        TransportOutDescription transportOutDescription;
        if (transport.equals(Constants.TRANSPORT_HTTP)) {

            // For HTTP Transport, no specific connection socket factory is required
            assert connectionSocketFactory == null;

            // set transport protocol for ingoing message (response)
            options.setTransportInProtocol(Constants.TRANSPORT_HTTP);

            // set transport out description
            transportOutDescription = new TransportOutDescription("http");

            // set the HTTP transport sender
            if (SUPropertiesHelper.isAxis1CompatibilityEnabled(extensions)) {
                sender = new Axis1SOAPFaultHTTPTransportSender();

                if (context.getLogger().isLoggable(Level.FINE)) {
                    context.getLogger().log(Level.FINE,
                            "Set the customized Axis 2 transport sender: "
                            + sender.getClass().getName());
                }
            } else {
                sender = new HTTPClient4TransportSender();
            }
        } else if (transport.equals(Constants.TRANSPORT_HTTPS)) {

            // For HTTPS Transport, a specific connection socket factory is required
            assert connectionSocketFactory != null;

            // set transport protocol for ingoing message (response)
            options.setTransportInProtocol(Constants.TRANSPORT_HTTPS);

            transportOutDescription = new TransportOutDescription("https");
            sender = new HTTPClient4TransportSender();
            
/*            final Registry socketFactoryRegistry = RegistryBuilder.create().register("https", sslsf).build();
            
            httpClientConnectionManager = this.createHttpClientConnectionManager(socketFactoryRegistry);

            HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(connManager).setConnectionManagerShared(true).build();

            ProtocolSocketFactory socketFactory;
            if (truststoreURL == null && keystoreURL == null) {
                socketFactory = new EasySSLProtocolSocketFactory();
                if (context.getLogger().isLoggable(Level.WARNING)) {
                    context.getLogger().log(Level.WARNING, "Client and server authentications are disabled.");
                }
            } else {
                socketFactory = new AuthSSLProtocolSocketFactory(keystoreURL, keystorePassword,
                        truststoreURL, truststorePassword);
            }

            int port = uri.getPort();
            if (port == -1) {
                port = DEFAULT_HTTPS_PORT;
            }

            final Protocol protocolHandler = new Protocol("https", socketFactory, port);
            options.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER, protocolHandler);
*/
        } else {
            throw new UnsupportedOperationException(String
                    .format("Transport protocol '%s' (extracted from url '%s') not supported.", transport,
                            this.serviceClientConfiguration.address));
        }
        
        // set http PROTOCOL parameter (request)
        try {
            transportOutDescription.addParameter(new Parameter("PROTOCOL", "HTTP/1.1"));
        } catch (final AxisFault af) {
            if (context.getLogger().isLoggable(Level.WARNING)) {
                context.getLogger().log(Level.WARNING,
                        "Can not set the http PROTOCOL parameter: " + af.getMessage());
            }
        }

        try {
            sender.init(configuration, transportOutDescription);
        } catch (final AxisFault e) {
            throw new MessagingException("Can not initialiaze the transport sender: "
                    + e.getMessage());
        }
        transportOutDescription.setSender(sender);

        // set the transport for outgoing message
        options.setTransportOut(transportOutDescription);

        // set chunked mode
        options.setProperty(HTTPConstants.CHUNKED, SUPropertiesHelper.retrieveChunkedMode(extensions));

        return null;
    }

    private HttpClientConnectionManager createHttpClientConnectionManager(final Registry socketFactoryRegistry) {

        // Configure the outgoing HTTP connection pool of this web-service client
        final PoolingHttpClientConnectionManager pollingHttpClientConnectionManager = socketFactoryRegistry != null ? new PoolingHttpClientConnectionManager(socketFactoryRegistry) : new PoolingHttpClientConnectionManager();
        // this default to the maximum number of JBI processors
        pollingHttpClientConnectionManager.setDefaultMaxPerRoute(this.serviceClientConfiguration.jbiProcessorMaxSize);
        // we use the same as the max number of processor: there can't be more open connections that the number of
        // processor running.
        pollingHttpClientConnectionManager.setMaxTotal(this.serviceClientConfiguration.jbiProcessorMaxSize);

        return pollingHttpClientConnectionManager;
    }

    /**
     * Engage a module
     * 
     * @param petalsServiceClient
     * @param moduleName
     * @throws AxisFault
     */
    private void engageModule(final PetalsServiceClient petalsServiceClient,
            final String moduleName) throws AxisFault {
        if (this.context.getLogger().isLoggable(Level.FINE)) {
            this.context.getLogger().fine("Engaging module " + moduleName);
        }
        petalsServiceClient.engageModule(moduleName);
    }

    /**
     * Engage the modules for the service client. The modules are available on
     * the service unit listener since they have been defined during SU
     * deployment.
     * 
     * @param petalsServiceClient
     * @throws AxisFault
     */
    private void engageModules(final PetalsServiceClient petalsServiceClient) throws AxisFault {
        final List modules = this.context.getModules();
        if (modules != null) {
            for (final String name : modules) {
                this.engageModule(petalsServiceClient, name);
            }
        }
    }

    @Override
    public ServiceClient create() throws Exception {

        if (this.context.getLogger().isLoggable(Level.FINE)) {
            this.context.getLogger()
                    .fine("Creating a service client for : " + this.serviceClientConfiguration.address
                            + ", with operation '" + this.serviceClientConfiguration.operation + "', and MEP '"
                            + this.serviceClientConfiguration.mep + "'");
        }

        final AxisService axisService = new AxisService(this.uuidGenerator.getNewID());
        final ClassLoader cl = this.context.getClassloader();
        axisService.setClassLoader(cl);

        final AxisOperation axisOperation;
        if (MEPConstants.IN_ONLY_PATTERN.equals(this.serviceClientConfiguration.mep)) {
            axisOperation = new OutOnlyAxisOperation(this.serviceClientConfiguration.operation);
        } else if (MEPConstants.ROBUST_IN_ONLY_PATTERN.equals(this.serviceClientConfiguration.mep)) {
            // TODO hack because of https://issues.apache.org/jira/browse/AXIS2-5718
            // With RobustOutOnlyAxisOperation the fault is thrown instead of being returned as desired
            axisOperation = new OutInAxisOperation(this.serviceClientConfiguration.operation);
            axisOperation.setMessageExchangePattern(WSDL2Constants.MEP_URI_ROBUST_OUT_ONLY);
        } else if (MEPConstants.IN_OPTIONAL_OUT_PATTERN.equals(this.serviceClientConfiguration.mep)
                || MEPConstants.IN_OUT_PATTERN.equals(this.serviceClientConfiguration.mep)) {
            axisOperation = new OutInAxisOperation(this.serviceClientConfiguration.operation);
        } else {
            axisOperation = null;
        }
        axisService.addOperation(axisOperation);

        // if it's null, it will never work anyway...
        if (axisOperation != null) {
            // not sure which one is used between this and the one set in the options...
            axisOperation.setSoapAction(this.serviceClientConfiguration.soapAction);
        }

        // create Options for the stub
        final Options options = createOptions();

        // add the service parameters
        try {
            AxisServicesHelper.addServiceParameters(this.context.getServiceParams(), axisService);
        } catch (final XMLStreamException | AxisFault e) {
            this.context.getLogger().log(Level.WARNING, "Can't add service parameters", e);
        }

        try {
            // TODO this actually does not depends on the operation, so why do we pool on that???
            final PetalsServiceClient petalsServiceClient = new PetalsServiceClient(this.configuration, axisService);
            petalsServiceClient.setOptions(options);

            // engage the Axis2 modules
            engageModules(petalsServiceClient);

            // Enable the right WS-Policy
            final Policy wssPolicy = SUPropertiesHelper.getWSSPolicy(this.context.getExtensions(),
                    context.getClassloader(), this.context.getLogger());
            if (wssPolicy != null) {
                axisService.getPolicySubject().attachPolicy(wssPolicy);
            }

            return petalsServiceClient;
        } catch (final AxisFault e) {
            throw new MessagingException("Can't create ServiceClient", e);
        }
    }

    @Override
    public PooledObject wrap(final ServiceClient obj) {
        return new DefaultPooledObject(obj);
    }

    @Override
    public void destroyObject(final PooledObject psc) throws Exception {
        final ServiceClient sc = psc.getObject();
        sc.cleanup();
        sc.cleanupTransport();
    }

    @Override
    public void activateObject(final PooledObject pooledServiceClient) throws Exception {
        final ServiceClient serviceClient = pooledServiceClient.getObject();

        // to avoid overriding settings that could impact other calls to this service client
        // we extend the option and will restore them after
        final Options originalOptions = serviceClient.getOptions();
        final Options options = new Options(originalOptions);
        options.setProperty(ORIGINAL_OPTIONS_TO_RESTORE_ON_RETURN, originalOptions);
    }

    @Override
    public void passivateObject(final PooledObject pooledServiceClient) throws Exception {
        final ServiceClient serviceClient = pooledServiceClient.getObject();

        // we should always cleanup the transport once the response has been read (and the response must have been read
        // once this service client is returned).
        try {
            serviceClient.cleanupTransport();
        } catch (final AxisFault e) {
            this.context.getLogger().log(Level.SEVERE, "Can't cleanup the transport as needed... this is not normal",
                    e);
        }

        // restore options
        final Object originalOption = serviceClient.getOptions().getProperty(ORIGINAL_OPTIONS_TO_RESTORE_ON_RETURN);
        if (originalOption instanceof Options) {
            serviceClient.setOptions((Options) originalOption);
        } else {
            this.context.getLogger()
                    .severe("Can't find the Options I stored to restore them later... this is not normal");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy