com.betfair.cougar.client.AbstractHttpExecutable.java Source code

Java tutorial

Introduction

Here is the source code for com.betfair.cougar.client.AbstractHttpExecutable.java

Source

/*
 * Copyright 2014, The Sporting Exchange Limited
 * Copyright 2015, Simon Mati Langford
 *
 * Licensed 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 com.betfair.cougar.client;

import com.betfair.cougar.api.ExecutionContext;
import com.betfair.cougar.api.security.IdentityToken;
import com.betfair.cougar.api.security.IdentityTokenResolver;
import com.betfair.cougar.client.exception.ExceptionTransformer;
import com.betfair.cougar.client.query.QueryStringGeneratorFactory;
import com.betfair.cougar.core.api.OperationBindingDescriptor;
import com.betfair.cougar.core.api.client.AbstractClientTransport;
import com.betfair.cougar.core.api.client.ExceptionFactory;
import com.betfair.cougar.core.api.client.TransportMetrics;
import com.betfair.cougar.core.api.ev.*;
import com.betfair.cougar.core.api.exception.*;
import com.betfair.cougar.core.api.tracing.Tracer;
import com.betfair.cougar.core.api.transcription.EnumDerialisationException;
import com.betfair.cougar.core.api.transcription.EnumUtils;
import com.betfair.cougar.core.api.transcription.Parameter;
import com.betfair.cougar.core.impl.tracing.TracingEndObserver;
import com.betfair.cougar.marshalling.api.databinding.DataBindingFactory;
import com.betfair.cougar.transport.api.protocol.http.HttpServiceBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.rescript.RescriptOperationBindingDescriptor;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;

import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPInputStream;

/**
 * Base executable which holds the logic of interacting with Execution Venue.
 * Note this marshals the request and un-marshals the response using the provided Transformable
 */
@ManagedResource
public abstract class AbstractHttpExecutable<HR> extends AbstractClientTransport {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpExecutable.class);

    private static final String REMOTE_ADDRESS_DESCRIPTION = "Remote Service Address";
    private static final String SERVICE_CONTEXT_PATH_DESCRIPTION = "Service Context Path";

    protected static final int MAX_TOTAL_CONNECTIONS = 128;
    protected static final String CONTENT_TYPE = "application/json";
    private static final String UNMARSHALL_ENCODING = "utf-8";
    protected static final String DEFAULT_REQUEST_UUID_HEADER = "X-UUID";
    protected static final String DEFAULT_REQUEST_UUID_PARENTS_HEADER = "X-UUID-PARENTS";
    protected static final int DEFAULT_HTTPS_PORT = 443;
    protected static final int DEFAULT_HTTP_PORT = 80;

    private final Map<OperationKey, OperationBindingDescriptor> descriptorMap = new HashMap<OperationKey, OperationBindingDescriptor>();
    private final HttpServiceBindingDescriptor serviceBindingDescriptor;
    private final MessageBuilder messageBuilder = new MessageBuilder();

    private AtomicReference<String> remoteAddressRef = new AtomicReference<String>("NOT ASSIGNED");
    private DataBindingFactory dataBindingFactory;
    private IdentityTokenResolver<HR, HR, X509Certificate[]> identityTokenResolver;
    private QueryStringGeneratorFactory queryStringGeneratorFactory;
    private ExceptionTransformer exceptionTransformer;
    private ExceptionFactory exceptionFactory;
    protected CougarRequestFactory<HR> requestFactory;
    private Tracer tracer;

    protected boolean transportSSLEnabled;
    protected boolean hostnameVerificationDisabled;
    private boolean gzipCompressionEnabled = true;
    protected boolean hardFailEnumDeserialisation;

    //Path to the keystore
    protected Resource httpsKeystore;
    //Password for that keystore
    protected String httpsKeyPassword;
    protected String httpsKeystoreType = KeyStore.getDefaultType();

    //The trust store fields are only necessary for 2 way SSL
    //Path to the trusted certificate store
    protected Resource httpsTruststore;
    //Trust store's password
    protected String httpsTrustPassword;
    protected String httpsTruststoreType = KeyStore.getDefaultType();

    // http timeout, -1 = not set
    protected int connectTimeout = -1;
    protected int idleTimeout = -1;

    protected AbstractHttpExecutable(final HttpServiceBindingDescriptor bindingDescriptor,
            CougarRequestFactory<HR> requestFactory, Tracer tracer) {
        this.serviceBindingDescriptor = bindingDescriptor;
        this.requestFactory = requestFactory;
        this.tracer = tracer;
    }

    protected int extractPortFromAddress() throws IOException {
        String remoteAddress = remoteAddressRef.get();
        if (remoteAddress == null || remoteAddress.equals("NOT ASSIGNED")) {
            throw new IllegalArgumentException("Remote address not set!");
        }
        URL url = new URL(remoteAddress);
        return url.getPort();
    }

    public void init() throws Exception {
        final OperationBindingDescriptor[] operationBindings = serviceBindingDescriptor.getOperationBindings();

        for (OperationBindingDescriptor binding : operationBindings) {
            descriptorMap.put(binding.getOperationKey(), binding);
        }

        requestFactory.setGzipCompressionEnabled(gzipCompressionEnabled);
    }

    @Override
    public void execute(final ExecutionContext ctx, final OperationKey key, final Object[] args,
            final ExecutionObserver obs, final ExecutionVenue executionVenue,
            final TimeConstraints timeConstraints) {

        final ClientCallContext callContext = CallContextFactory.createSubContext(ctx);

        tracer.startCall(ctx.getRequestUUID(), callContext.getRequestUUID(), key);

        final OperationDefinition operationDefinition = executionVenue.getOperationDefinition(key);

        final Parameter[] parameters = operationDefinition.getParameters();
        final RescriptOperationBindingDescriptor operationBinding = (RescriptOperationBindingDescriptor) descriptorMap
                .get(key.getLocalKey());

        final Message message = messageBuilder.build(args, parameters, operationBinding);
        String contextPath = serviceBindingDescriptor.getServiceContextPath();
        String remoteAddress = remoteAddressRef.get();
        if (remoteAddress.endsWith("/") && contextPath.startsWith("/")) {
            contextPath = contextPath.substring(1);
        }
        final String uri = remoteAddress + contextPath + "v"
                + serviceBindingDescriptor.getServiceVersion().getMajor() + operationBinding.getURI();
        final String queryString = queryStringGeneratorFactory.getQueryStringGenerator()
                .generate(message.getQueryParmMap());
        final String httpMethod = operationBinding.getHttpMethod();

        // create http request

        HR request = requestFactory.create(uri + queryString, httpMethod, message,
                dataBindingFactory.getMarshaller(), CONTENT_TYPE, callContext, timeConstraints);

        Exception exception = null;
        Object result = null;
        InputStream inputStream = null;

        if (identityTokenResolver != null && getIdentityResolver() != null
                && identityTokenResolver.isRewriteSupported()) {
            final List<IdentityToken> identityTokens = getIdentityResolver().tokenise(ctx.getIdentity());
            identityTokenResolver.rewrite(identityTokens, request);
            if (LOGGER.isDebugEnabled()) {
                StringBuilder sb = new StringBuilder();
                for (IdentityToken it : identityTokens) {
                    if (sb.length() > 0) {
                        sb.append(",");
                    }
                    sb.append(it.getName()).append("=").append(it.getValue());
                }
                LOGGER.debug("Rewrote tokens " + sb + " to http request");
            }
        }

        // Send Request

        sendRequest(request,
                new TracingEndObserver(tracer, obs, ctx.getRequestUUID(), callContext.getRequestUUID(), key),
                operationDefinition);

    }

    protected interface CougarHttpResponse {

        InputStream getEntity() throws IOException;

        List<String> getContentEncoding();

        int getResponseStatus();

        String getServerIdentity();

        long getResponseSize();
    }

    protected void processResponse(final CougarHttpResponse response, final ExecutionObserver obs,
            final OperationDefinition definition) {
        Exception exception = null;
        Object result = null;
        InputStream inputStream = null;
        final RescriptOperationBindingDescriptor operationBinding = (RescriptOperationBindingDescriptor) descriptorMap
                .get(definition.getOperationKey().getLocalKey());
        try {
            if (response.getEntity() == null) {
                exception = new CougarClientException(ServerFaultCode.RemoteCougarCommunicationFailure,
                        "No response returned by server");
            } else {
                inputStream = response.getEntity();
                if (gzipCompressionEnabled && !gzipHandledByTransport()) {
                    List<String> codecs = response.getContentEncoding();
                    for (String codecName : codecs) {
                        if ("gzip".equals(codecName) || "x-gzip".equals(codecName)) {
                            inputStream = new GZIPInputStream(inputStream);
                        }
                    }
                }
                EnumUtils.setHardFailureForThisThread(hardFailEnumDeserialisation);
                if (response.getResponseStatus() == HttpStatus.SC_OK) {
                    if (!operationBinding.voidReturnType()) {
                        try {
                            result = dataBindingFactory.getUnMarshaller().unmarshall(inputStream,
                                    definition.getReturnType(), UNMARSHALL_ENCODING, true);
                        } catch (Exception e2) {
                            throw clientException(response, e2);
                        }
                    }
                } else if (response.getResponseStatus() == HttpStatus.SC_NOT_FOUND) {
                    // IN this case, we know there will be no exception body, so just stick on a new Exception
                    exception = new CougarClientException(ServerFaultCode.NoSuchService,
                            "The server did not recognise the URL.");
                } else {
                    try {
                        exception = exceptionTransformer.convert(inputStream, exceptionFactory,
                                response.getResponseStatus());
                    } catch (Exception e2) {
                        throw clientException(response, e2);
                    }
                }
            }
        } catch (final Exception e) {
            if (e instanceof CougarClientException) {
                exception = e;
            } else if (e instanceof EnumDerialisationException) {
                exception = new CougarClientException(
                        CougarMarshallingException.unmarshallingException("json", "Enum failure", e, true));
            } else if (e instanceof CougarException) {
                exception = new CougarClientException((CougarException) e);
            } else {
                exception = new CougarClientException(ServerFaultCode.RemoteCougarCommunicationFailure,
                        "Exception occurred in Client: " + e.getMessage(), e);
            }
        } finally {
            IOUtils.closeQuietly(inputStream);
        }

        if (exception == null) {
            obs.onResult(new ClientExecutionResult(result, response.getResponseSize()));
        } else {
            obs.onResult(new ClientExecutionResult(exception, response.getResponseSize()));
        }
        // end request callback
    }

    private CougarClientException clientException(CougarHttpResponse response, Exception e) {
        if (e instanceof CougarClientException) {
            return (CougarClientException) e;
        }
        if (e instanceof CougarException) {
            return new CougarClientException(((CougarException) e).getServerFaultCode(), e.getMessage(), e,
                    !isDefinitelyCougarResponse(response));
        }
        ServerFaultCode serverFaultCode = ServerFaultCode.RemoteCougarCommunicationFailure;
        String message = "Unknown error communicating with remote Cougar service";
        switch (response.getResponseStatus()) {
        case HttpStatus.SC_NOT_FOUND:
            serverFaultCode = ServerFaultCode.NoSuchOperation;
            message = "Service not found";
        }
        return new CougarClientException(serverFaultCode, message, e, !isDefinitelyCougarResponse(response));
    }

    protected void processException(ExecutionObserver obs, Throwable t, String url) {
        ServerFaultCode serverFaultCode = ServerFaultCode.RemoteCougarCommunicationFailure;
        if (t instanceof SocketTimeoutException) {
            serverFaultCode = ServerFaultCode.Timeout;
        }
        processException(obs, t, url, serverFaultCode);
    }

    protected void processException(ExecutionObserver obs, Throwable t, String url,
            ServerFaultCode serverFaultCode) {
        Exception exception = new CougarClientException(serverFaultCode,
                "Exception occurred in Client: " + t.getMessage() + ": " + url, t);
        obs.onResult(new ClientExecutionResult(exception, 0));
    }

    protected boolean gzipHandledByTransport() {
        return false;
    }

    protected abstract void sendRequest(HR request, ExecutionObserver obs, OperationDefinition operationDefinition);

    /**
     * has the responding server identified itself as Cougar
     *
     * Note that due to legacy servers/network infrastructure a 'false negative' is possible.
     * in other words although a 'true' response confirms that the server has identified itself Cougar,
     * a 'false' does not confirm that a server IS NOT Cougar. The cougar header could have been stripped out
     * by network infrastructure, or the server could still be a legacy Cougar which does not provide this header.
     *
     * @param response http response
     * @return boolean has the responding server identified itself as Cougar
     */
    private boolean isDefinitelyCougarResponse(CougarHttpResponse response) {
        String ident = response.getServerIdentity();
        if (ident != null && ident.contains("Cougar 2")) {
            return true;
        }
        return false;
    }

    @ManagedAttribute(description = REMOTE_ADDRESS_DESCRIPTION)
    public void setRemoteAddress(final String host) {
        this.remoteAddressRef.set(host);
    }

    @ManagedAttribute(description = REMOTE_ADDRESS_DESCRIPTION)
    public String getRemoteAddress() {
        return remoteAddressRef.get();
    }

    @ManagedAttribute(description = SERVICE_CONTEXT_PATH_DESCRIPTION)
    public String getServiceContextPath() {
        if (serviceBindingDescriptor != null) {
            return serviceBindingDescriptor.getServiceContextPath();
        } else {
            return "";
        }
    }

    /**
     * Return some information about the current transport.
     *
     * @return the transport metrics
     */
    public abstract TransportMetrics getTransportMetrics();

    public void setDataBindingFactory(DataBindingFactory dataBindingFactory) {
        this.dataBindingFactory = dataBindingFactory;
    }

    public void setQueryStringGeneratorFactory(QueryStringGeneratorFactory queryStringGeneratorFactory) {
        this.queryStringGeneratorFactory = queryStringGeneratorFactory;
    }

    public void setIdentityTokenResolver(IdentityTokenResolver<HR, HR, X509Certificate[]> identityTokenResolver) {
        this.identityTokenResolver = identityTokenResolver;
    }

    public void setExceptionTransformer(ExceptionTransformer exceptionTransformer) {
        this.exceptionTransformer = exceptionTransformer;
    }

    public void setExceptionFactory(ExceptionFactory exceptionFactory) {
        this.exceptionFactory = exceptionFactory;
    }

    public void setTransportSSLEnabled(boolean transportSSLEnabled) {
        this.transportSSLEnabled = transportSSLEnabled;
    }

    public void setHttpsKeyPassword(String httpsKeyPassword) {
        this.httpsKeyPassword = httpsKeyPassword;
    }

    public void setHttpsKeystore(Resource httpsKeystore) {
        this.httpsKeystore = httpsKeystore;
    }

    public void setHttpsKeystoreType(String httpsKeystoreType) {
        this.httpsKeystoreType = httpsKeystoreType;
    }

    public void setHttpsTrustPassword(String httpsTrustPassword) {
        this.httpsTrustPassword = httpsTrustPassword;
    }

    public void setHttpsTruststore(Resource httpsTruststore) {
        this.httpsTruststore = httpsTruststore;
    }

    public void setHttpsTruststoreType(String httpsTruststoreType) {
        this.httpsTruststoreType = httpsTruststoreType;
    }

    public void setConnectTimeout(int timeout) {
        this.connectTimeout = timeout;
    }

    public void setIdleTimeout(int idleTimeout) {
        this.idleTimeout = idleTimeout;
    }

    public void setHostnameVerificationDisabled(final boolean hostnameVerificationDisabled) {
        this.hostnameVerificationDisabled = hostnameVerificationDisabled;
    }

    public void setRequestFactory(CougarRequestFactory<HR> requestFactory) {
        this.requestFactory = requestFactory;
    }

    @ManagedAttribute
    public int getIdleTimeout() {
        return idleTimeout;
    }

    @ManagedAttribute
    public int getConnectTimeout() {
        return connectTimeout;
    }

    @ManagedAttribute
    public String getHttpsTruststoreType() {
        return httpsTruststoreType;
    }

    @ManagedAttribute
    public Resource getHttpsTruststore() {
        return httpsTruststore;
    }

    @ManagedAttribute
    public String getHttpsKeystoreType() {
        return httpsKeystoreType;
    }

    @ManagedAttribute
    public Resource getHttpsKeystore() {
        return httpsKeystore;
    }

    @ManagedAttribute
    public boolean isHostnameVerificationDisabled() {
        return hostnameVerificationDisabled;
    }

    @ManagedAttribute
    public boolean isTransportSSLEnabled() {
        return transportSSLEnabled;
    }

    @ManagedAttribute
    public boolean isHardFailEnumDeserialisation() {
        return hardFailEnumDeserialisation;
    }

    public void setHardFailEnumDeserialisation(boolean hardFailEnumDeserialisation) {
        this.hardFailEnumDeserialisation = hardFailEnumDeserialisation;
    }

    @ManagedAttribute
    public boolean isGzipCompressionEnabled() {
        return gzipCompressionEnabled;
    }

    @ManagedAttribute
    public void setGzipCompressionEnabled(boolean gzipCompressionEnabled) {
        this.gzipCompressionEnabled = gzipCompressionEnabled;
    }

}