Package com.betfair.cougar.transport.impl.protocol.http.soap

Source Code of com.betfair.cougar.transport.impl.protocol.http.soap.SoapTransportCommandProcessor

/*
* Copyright 2013, The Sporting Exchange Limited
*
* 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.transport.impl.protocol.http.soap;

import com.betfair.cougar.api.ExecutionContextWithTokens;
import com.betfair.cougar.api.ResponseCode;
import com.betfair.cougar.api.security.IdentityToken;
import com.betfair.cougar.api.security.InferredCountryResolver;
import com.betfair.cougar.core.api.OperationBindingDescriptor;
import com.betfair.cougar.core.api.ServiceBindingDescriptor;
import com.betfair.cougar.core.api.ev.ExecutionResult;
import com.betfair.cougar.core.api.ev.OperationDefinition;
import com.betfair.cougar.core.api.ev.OperationKey;
import com.betfair.cougar.core.api.ev.TimeConstraints;
import com.betfair.cougar.core.api.exception.*;
import com.betfair.cougar.core.api.fault.CougarFault;
import com.betfair.cougar.core.api.fault.FaultController;
import com.betfair.cougar.core.api.fault.FaultDetail;
import com.betfair.cougar.core.api.transcription.*;
import com.betfair.cougar.core.impl.DefaultTimeConstraints;
import com.betfair.cougar.logging.CougarLogger;
import com.betfair.cougar.logging.CougarLoggingUtils;
import com.betfair.cougar.marshalling.impl.databinding.xml.SchemaValidationFailureParser;
import com.betfair.cougar.transport.api.CommandResolver;
import com.betfair.cougar.transport.api.ExecutionCommand;
import com.betfair.cougar.transport.api.RequestTimeResolver;
import com.betfair.cougar.transport.api.TransportCommand.CommandStatus;
import com.betfair.cougar.transport.api.protocol.http.GeoLocationDeserializer;
import com.betfair.cougar.transport.api.protocol.http.HttpCommand;
import com.betfair.cougar.transport.api.protocol.http.soap.SoapIdentityTokenResolver;
import com.betfair.cougar.transport.api.protocol.http.soap.SoapOperationBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.soap.SoapServiceBindingDescriptor;
import com.betfair.cougar.transport.impl.protocol.http.AbstractTerminateableHttpCommandProcessor;
import com.betfair.cougar.util.geolocation.GeoIPLocator;
import com.betfair.cougar.util.stream.ByteCountingInputStream;
import com.betfair.cougar.util.stream.ByteCountingOutputStream;
import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.impl.traverse.OMChildrenNamespaceIterator;
import org.apache.axiom.soap.*;
import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.util.StreamUtils;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stax.StAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.*;
import java.util.logging.Level;

/**
* TransportCommandProcessor for the SOAP protocol.
* Responsible for resolving the operation and arguments from the command,
* and for writing the result or exception from the operation to the response.
*/
@ManagedResource
public class SoapTransportCommandProcessor extends AbstractTerminateableHttpCommandProcessor {
    private static final String SECURITY_PREFIX = "sec";
    private static final String SECURITY_NAMESPACE = "http://www.betfair.com/security/";
    private static final String SECURITY_CREDENTIALS = "Credentials";

    private static final CougarLogger logger = CougarLoggingUtils.getLogger(SoapTransportCommandProcessor.class);

    private Map<String, SoapOperationBinding> bindings = new HashMap<String, SoapOperationBinding>();

    private boolean schemaValidationEnabled;
    private SchemaValidationFailureParser schemaValidationFailureParser;

    public SoapTransportCommandProcessor(GeoIPLocator geoIPLocator,
                                         GeoLocationDeserializer deserializer, String uuidHeader, String requestTimeoutHeader, RequestTimeResolver requestTimeResolver, SchemaValidationFailureParser schemaValidationFailureParser) {
        this(geoIPLocator, deserializer, uuidHeader, requestTimeoutHeader, requestTimeResolver, schemaValidationFailureParser, null);
    }

    // for testing only
    SoapTransportCommandProcessor(GeoIPLocator geoIPLocator, GeoLocationDeserializer deserializer, String uuidHeader,
                                  String requestTimeoutHeader, RequestTimeResolver requestTimeResolver, SchemaValidationFailureParser schemaValidationFailureParser,
                                  InferredCountryResolver<HttpServletRequest> countryResolver) {
        super(geoIPLocator, deserializer, uuidHeader, countryResolver, requestTimeoutHeader, requestTimeResolver);
        setName("SoapTransportCommandProcessor");
        this.schemaValidationFailureParser = schemaValidationFailureParser;
    }

    @ManagedAttribute
    public boolean isSchemaValidationEnabled() {
        return schemaValidationEnabled;
    }

    @ManagedAttribute
    public void setSchemaValidationEnabled(boolean schemaValidationEnabled) {
        this.schemaValidationEnabled = schemaValidationEnabled;
    }

    @Override
    public void onCougarStart() {
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        for (ServiceBindingDescriptor sd : getServiceBindingDescriptors()) {
            SoapServiceBindingDescriptor soapServiceDesc = (SoapServiceBindingDescriptor) sd;
            try {
                // we'll load the schema content and create a Schema object once, as this is threadsafe and so can be reused
                // this should cut down on some memory usage and remove schema parsing from the critical path when validating
                try (InputStream is = soapServiceDesc.getClass().getClassLoader().getResourceAsStream(soapServiceDesc.getSchemaPath())) {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    StreamUtils.copy(is, baos);
                    String schemaContent = baos.toString();
                    Schema schema = schemaFactory.newSchema(new StreamSource(new StringReader(schemaContent)));
                    String uriVersionStripped = stripMinorVersionFromUri(soapServiceDesc.getServiceContextPath() + soapServiceDesc.getServiceVersion());
                    for (OperationBindingDescriptor desc : soapServiceDesc.getOperationBindings()) {
                        SoapOperationBindingDescriptor soapOpDesc = (SoapOperationBindingDescriptor) desc;
                        OperationDefinition opDef = getOperationDefinition(soapOpDesc.getOperationKey());
                        String operationName = uriVersionStripped + "/" + soapOpDesc.getRequestName().toLowerCase();
                        bindings.put(operationName,
                                new SoapOperationBinding(opDef, soapOpDesc,
                                        soapServiceDesc, schema));
                    }
                }
            }
            catch (IOException | SAXException e) {
                throw new CougarFrameworkException("Error loading schema", e);
            }
        }
    }

    @Override
    protected CommandResolver<HttpCommand> createCommandResolver(
            final HttpCommand command) {
        String operationName = null;
        ByteCountingInputStream in = null;
        try {
            in = createByteCountingInputStream(command.getRequest().getInputStream());
            XMLInputFactory factory = XMLInputFactory.newInstance();
            factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
            XMLStreamReader parser = factory.createXMLStreamReader(in);
            StAXSOAPModelBuilder builder = new StAXSOAPModelBuilder(parser);
            final SOAPHeader header = builder.getSOAPEnvelope().getHeader();
            final OMElement credentialElement = getCredentialElement(header);

            final OMElement requestNode = builder.getSOAPEnvelope().getBody().getFirstElement();

            operationName = requestNode.getLocalName();
            String fullPathToOperationName = stripMinorVersionFromUri(command.getOperationPath()) + "/" + operationName.toLowerCase();
            final SoapOperationBinding binding = bindings.get(fullPathToOperationName);
            if (binding != null) {

                if (schemaValidationEnabled) {
                    Schema schema = binding.getSchema();
                    Validator validator = schema.newValidator();
                    validator.validate(new StAXSource(requestNode.getXMLStreamReader(true)));
                }

                final ByteCountingInputStream finalIn = in;
                return new SingleExecutionCommandResolver<HttpCommand>() {

                    private ExecutionContextWithTokens context;
                    private ExecutionCommand exec;

                    @Override
                    public ExecutionContextWithTokens resolveExecutionContext() {
                        if (context == null) {
                            context = SoapTransportCommandProcessor.this.resolveExecutionContext(command, credentialElement, command.getClientX509CertificateChain());
                        }
                        return context;
                    }

                    @Override
                    public ExecutionCommand resolveExecutionCommand() {
                        if (exec == null) {
                            exec = SoapTransportCommandProcessor.this.resolveExecutionCommand(binding, command,
                                    resolveExecutionContext(), requestNode, finalIn);
                        }
                        return exec;
                    }
                };
            }
        } catch (CougarException e) {
            throw e;
        } catch (SAXException e) {
            if (e.getException() instanceof TransformerException) {
                TransformerException te = (TransformerException) e.getException();
                if (te.getException() instanceof XMLStreamException) {
                    XMLStreamException se = (XMLStreamException) te.getException();
                    if (se.getCause() instanceof SAXParseException) {
                        SAXParseException spe = (SAXParseException) se.getCause();
                        CougarException ce = schemaValidationFailureParser.parse(spe, "soap", false);
                        if (ce != null) {
                            throw ce;
                        }
                    }
                }
            }
            throw CougarMarshallingException.unmarshallingException("soap", e, false);
        } catch (Exception e) {
            throw CougarMarshallingException.unmarshallingException("soap", e, false);
        } finally {
            try {
                if (in != null) in.close();
            } catch (IOException ie) {
                throw CougarMarshallingException.unmarshallingException("soap", ie, false);
            }
        }

        throw new CougarValidationException(ServerFaultCode.NoSuchOperation,
                "The SOAP request could not be resolved to an operation");
    }

    private ExecutionCommand resolveExecutionCommand(
            final SoapOperationBinding operationBinding,
            final HttpCommand command, final ExecutionContextWithTokens context,
            OMElement requestNode, ByteCountingInputStream in) {
        final Object[] args = readArgs(operationBinding, requestNode);
        final long bytesRead = in.getCount();
        final TimeConstraints realTimeConstraints = DefaultTimeConstraints.rebaseFromNewStartTime(context.getRequestTime(), readRawTimeConstraints(command.getRequest()));
        return new ExecutionCommand() {
            public Object[] getArgs() {
                return args;
            }

            public OperationKey getOperationKey() {
                return operationBinding.getOperationKey();
            }

            @Override
            public TimeConstraints getTimeConstraints() {
                return realTimeConstraints;
            }

            public void onResult(ExecutionResult result) {
                if (command.getStatus() == CommandStatus.InProcess) {
                    try {
                        if (result.getResultType() == ExecutionResult.ResultType.Fault) {
                            command.getResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                            writeResponse(command, operationBinding, null, result.getFault(), context, bytesRead);
                        } else if (result.getResultType() == ExecutionResult.ResultType.Success) {
                            writeResponse(command, operationBinding, result.getResult(), null, context, bytesRead);
                        }
                    } finally {
                        command.onComplete();
                    }
                }
            }
        };
    }

    @Override
    protected void writeErrorResponse(HttpCommand command, ExecutionContextWithTokens context, CougarException e) {
        incrementErrorsWritten();
        if (command.getStatus() == CommandStatus.InProcess) {
            try {
                // if we have a fault, then for SOAP we must return a 500: http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383529
                command.getResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                writeResponse(command, null, null, e, resolveContextForErrorHandling(context, command), 0);
            } finally {
                command.onComplete();
            }
        }
    }

    private Object[] readArgs(SoapOperationBinding operationBinding,
                              OMElement requestNode) {
        final Parameter[] params = operationBinding.getOperationDefinition()
                .getParameters();
        final Object[] args = new Object[params.length];
        EnumUtils.setHardFailureForThisThread(hardFailEnumDeserialisation);
        TranscriptionInput in = new XMLTranscriptionInput(requestNode);
        try {
            for (int i = 0; i < params.length; i++) {
                args[i] = readArg(in.readObject(params[i], false), params[i]);
            }
        } catch (EnumDerialisationException ce) {
            throw CougarMarshallingException.unmarshallingException("soap", ce.getMessage(), ce.getCause(), false);
        } catch (CougarException ce) {
            throw ce;
        } catch (Exception e) {
            throw new CougarFrameworkException("Failed to unmarshall SOAP arguments", e);
        }
        return args;
    }


    private Object readArg(Object arg, Parameter param) {
        // Special handling of enums
        // If an enum is not involved just return the already deserialised object

        Object returnValue = arg;
        // if the top level param is an enum we need to convert it as this is normally left to the Transcribable interface
        if (param.getParameterType().getType() == ParameterType.Type.ENUM) {
            returnValue = toEnum(param.getParameterType(), (String) arg, param.getName(), hardFailEnumDeserialisation);
        }

        // Handle Collection of enums. Only List and Sets supported as of now
        if ((param.getParameterType().getType() == ParameterType.Type.SET || param.getParameterType().getType() == ParameterType.Type.LIST) &&
                param.getParameterType().getComponentTypes()[0].getType() == ParameterType.Type.ENUM && arg != null) {
            Collection result = ((param.getParameterType().getType() == ParameterType.Type.SET) ? new HashSet() : new ArrayList());
            for (String enumTextValue : (Collection<String>) arg) {
                result.add(toEnum(param.getParameterType().getComponentTypes()[0], enumTextValue, param.getName(), hardFailEnumDeserialisation));
            }
            returnValue = result;
        }

        return returnValue;
    }

    // Deserialise enums explicitly
    private Object toEnum(ParameterType parameterType, String enumTextValue, String paramName, boolean hardFailEnumDeserialisation) {
        try {
            return EnumUtils.readEnum(parameterType.getImplementationClass(), enumTextValue, hardFailEnumDeserialisation);
        } catch (Exception e) {
            throw XMLTranscriptionInput.exceptionDuringDeserialisation(parameterType, paramName, e, false);
        }
    }

    private void writeResponse(HttpCommand command, SoapOperationBinding binding, Object result, CougarException error,
                               ExecutionContextWithTokens context, long bytesRead) {
        MediaType mediaType = MediaType.TEXT_XML_TYPE;
        ByteCountingOutputStream out = null;
        long bytesWritten = 0;
        boolean logAccess = true;
        try {
            command.getResponse().setContentType(mediaType.toString());
            out = new ByteCountingOutputStream(command.getResponse().getOutputStream());
            SOAPFactory factory = OMAbstractFactory.getSOAP11Factory();
            SOAPEnvelope envelope = factory.createSOAPEnvelope();
            SOAPHeader header = factory.createSOAPHeader(envelope);
            writeHeaders(factory, header, command, context);
            SOAPBody body = factory.createSOAPBody(envelope);
            writeError(factory, binding, body, error);
            writeBody(factory, binding, body, result);
            envelope.serialize(out);
            bytesWritten = out.getCount();
        } catch (Exception e) {
            CougarException ce = handleResponseWritingIOException(e, result.getClass());

            if (ce.getResponseCode() == ResponseCode.CantWriteToSocket) {
                // Log in the access log what's happened and end it all.
                error = ce;
            } else if (error == null) {
                // It was a normal response, so write an error instead
                writeErrorResponse(command, context, ce);
                logAccess = false; // We're coming back in here, so log the access then.
            } else {
                // Not much to do here - it's already an error and it's failed to send
                logger.log(Level.WARNING, "Failed to write SOAP error", e);
            }
        } finally {
            closeStream(out);
        }
        if (logAccess) {
            logAccess(command,
                    context, bytesRead,
                    bytesWritten, mediaType,
                    mediaType,
                    error != null ? error.getResponseCode() : ResponseCode.Ok);
        }
    }

    private void writeHeaders(final SOAPFactory factory, final SOAPHeader header, HttpCommand command, ExecutionContextWithTokens context)
            throws Exception {
        final SoapIdentityTokenResolver identityTokenResolver = (SoapIdentityTokenResolver) command.getIdentityTokenResolver();
        if (context != null && context.getIdentity() != null && identityTokenResolver != null) {

            writeIdentity(context.getIdentityTokens(), new IdentityTokenIOAdapter() {
                @Override
                public void rewriteIdentityTokens(List<IdentityToken> identityTokens) {
                    OMElement element = header.addHeaderBlock(SECURITY_CREDENTIALS, factory.createOMNamespace(SECURITY_NAMESPACE, SECURITY_PREFIX));
                    identityTokenResolver.rewrite(identityTokens, element);
                }

                @Override
                public boolean isRewriteSupported() {
                    return identityTokenResolver.isRewriteSupported();
                }
            });
        }
    }

    /**
     * Changes made to this method as part of workaround for DE5417 (bug in isEquals method in Axiom code) - altered lines commented
     */
    private OMElement getCredentialElement(SOAPHeader header) {
        if (header != null) {
            //Iterator it = header.getChildrenWithNamespaceURI(SECURITY_NAMESPACE); // Line commented out
            Iterator it = new WorkAroundOMChildrenNamespaceIterator(header.getFirstOMChild(), SECURITY_NAMESPACE); // Line added
            if (it.hasNext()) {
                OMElement element = (OMElement) it.next();
                if (element.getLocalName().equalsIgnoreCase(SECURITY_CREDENTIALS)) {
                    return element;
                } else {
                    logger.log(Level.FINE, "Unexpected security header arrived: %s", element.getLocalName());
                }
            }
        }
        return null;

    }

    private void writeBody(SOAPFactory factory, SoapOperationBinding binding, SOAPBody body, Object result)
            throws Exception {
        if (result != null) {
            OMNamespace ns = factory.createOMNamespace(binding
                    .getServiceBindingDescriptor().getNamespaceURI(), XMLConstants.DEFAULT_NS_PREFIX);
            OMElement resultNode = factory.createOMElement(binding
                    .getBindingDescriptor().getResponseName(), ns);
            TranscriptionOutput out = new XMLTranscriptionOutput(resultNode, ns, factory);
            out.writeObject(result, new Parameter("response", binding.getOperationDefinition()
                    .getReturnType(), true), false);
            body.addChild(resultNode);
        }
    }

    private void writeError(SOAPFactory factory, SoapOperationBinding binding, SOAPBody body, CougarException error) throws Exception {
        if (error != null) {
            SOAPFault soapFault = factory.createSOAPFault(body);
            if (error.getFault() != null) {
                createFaultCode(factory, soapFault, error.getFault());
                createFaultString(factory, soapFault, error.getFault());
                createFaultDetail(factory, soapFault, error.getFault(), binding);
            }
        }
    }

    private void createFaultCode(SOAPFactory factory, SOAPFault soapFault, CougarFault fault) {
        SOAPFaultCode code = factory.createSOAPFaultCode(soapFault);
        code.setText(factory.getNamespace().getPrefix() + ":" + fault.getFaultCode().name());
    }

    private void createFaultString(SOAPFactory factory, SOAPFault soapFault, CougarFault fault) {
        SOAPFaultReason reason = factory.createSOAPFaultReason(soapFault);
        reason.setText(fault.getErrorCode());
    }

    private void createFaultDetail(SOAPFactory factory, SOAPFault soapFault, CougarFault fault, SoapOperationBinding binding) throws Exception {
        SOAPFaultDetail soapFaultDetail = factory.createSOAPFaultDetail(soapFault);
        FaultDetail detail = fault.getDetail();
        if (detail != null) {
            List<String[]> faultMessages = detail.getFaultMessages();

            if (faultMessages != null && faultMessages.size() > 0) {
                OMNamespace ns = factory.createOMNamespace(binding
                        .getServiceBindingDescriptor().getNamespaceURI(), binding
                        .getServiceBindingDescriptor().getNamespacePrefix());


                OMElement faultNode = factory.createOMElement(detail.getFaultName(), ns);
                for (String[] msg : faultMessages) {
                    OMElement messageNode = factory.createOMElement(msg[0], ns);
                    messageNode.setText(msg[1]);
                    faultNode.addChild(messageNode);
                }
                soapFaultDetail.addChild(faultNode);
            }

            if (FaultController.getInstance().isDetailedFaults()) {
                OMElement stackTrace = factory.createOMElement(new QName("trace"));
                stackTrace.setText(detail.getStackTrace());
                soapFaultDetail.addChild(stackTrace);

                OMElement detailedMessage = factory.createOMElement(new QName("message"));
                detailedMessage.setText(detail.getDetailMessage());
                soapFaultDetail.addChild(detailedMessage);
            }

        }
    }

    /**
     * Extending OMChildrenNamespaceIterator in order to override bug in isEquals method as part of workaround for DE5417
     */
    private static class WorkAroundOMChildrenNamespaceIterator extends OMChildrenNamespaceIterator {

        public WorkAroundOMChildrenNamespaceIterator(OMNode currentChild, String uri) {
            super(currentChild, uri);
        }

        /**
         * This version of equals returns true if the local parts match. (Overridden to workaround bug in isEquals method in Axiom code)
         *
         * @param searchQName
         * @param currentQName
         * @return true if equals
         */
        @Override
        public boolean isEqual(QName searchQName, QName currentQName) {
            return searchQName.getNamespaceURI().equals(currentQName.getNamespaceURI());
        }
    }
}
TOP

Related Classes of com.betfair.cougar.transport.impl.protocol.http.soap.SoapTransportCommandProcessor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.