//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   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 de.ugoe.cs.autoquest.plugin.http;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Level;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.plugin.http.eventcore.EqualSOAPDataMap;
import de.ugoe.cs.autoquest.plugin.http.eventcore.SOAPEventType;
import de.ugoe.cs.autoquest.plugin.http.eventcore.SimpleSOAPEventType;
import de.ugoe.cs.autoquest.plugin.http.eventcore.SimpleSOAPEventType.CallType;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * Utilities for working with SOAP events. Their main task is to simply working with
 * {@link SimpleSOAPEventType} and {@link SOAPEventType}.
 * </p>
 * 
 * @author Steffen Herbold
 */
public class SOAPUtils {

    /**
     * <p>
     * Defines how sequences are ordered:
     * <ul>
     * <li>REQUEST: by the request order id</li>
     * <li>RESPONSE: by the response order id</li>
     * </ul>
     * </p>
     * 
     * @author Steffen Herbold
     */
    public enum SequenceOrder {
        REQUEST, RESPONSE
    }

    /**
     * <p>
     * Replaces events with a {@link SOAPEventType} with new events of {@link SimpleSOAPEventType}
     * and no target.
     * </p>
     * 
     * @param sequences
     *            sequences where the events are replaced
     * @param keepOnlySOAPEvents
     *            if true, all events that are not of SOAPEventType or SimpleSOAPEventType are
     *            removed
     * @return sequences with {@link SimpleSOAPEventType}s instead of {@link SOAPEventType}s
     */
    public static Collection<List<Event>> convertToSimpleSOAPEvent(Collection<List<Event>> sequences,
                                                                   boolean keepOnlySOAPEvents)
    {
        Collection<List<Event>> newSequences = new LinkedList<>();
        EqualSOAPDataMap equalSOAPDataMap = new EqualSOAPDataMap();
        for (List<Event> sequence : sequences) {
            List<Event> newSequence = null;
            if (sequence != null) {
                newSequence = new LinkedList<>();
                for (Event event : sequence) {
                    if (event.getType() instanceof SOAPEventType) {
                        SOAPEventType eventType = (SOAPEventType) event.getType();
                        newSequence.add(new Event(new SimpleSOAPEventType(eventType
                            .getCalledMethod(), eventType.getServiceName(), eventType
                            .getClientName(), eventType.getSoapRequestBody(), equalSOAPDataMap,
                                                                          CallType.REQUEST)));
                    }
                    else {
                        if (!keepOnlySOAPEvents || event.getType() instanceof SimpleSOAPEventType) {
                            newSequence.add(event);
                        }
                    }
                }
            }
            newSequences.add(newSequence);
        }
        return newSequences;
    }

    /**
     * <p>
     * Removes all non SOAP events from the contained sequences
     * </p>
     * 
     * @param sequences
     *            sequences where the events are replaced
     */
    public static Collection<List<Event>> removeNonSOAPEvents(Collection<List<Event>> sequences) {
        Collection<List<Event>> soapOnlySequences = new LinkedList<>();
        for (List<Event> sequence : sequences) {
            soapOnlySequences.add(removeNonSOAPEvents(sequence));
        }
        return soapOnlySequences;
    }

    /**
     * <p>
     * Removes all non SOAP events from a sequence
     * </p>
     * 
     * @param sequence
     *            sequence where the events are replaced
     */
    public static List<Event> removeNonSOAPEvents(List<Event> sequence) {
        List<Event> soapOnlySequence = new LinkedList<>();
        for (Event event : sequence) {
            if (event.getType() instanceof SOAPEventType) {
                soapOnlySequence.add(event);
            }
        }
        return soapOnlySequence;
    }

    /**
     * <p>
     * Helper function to get the name of a service from a SOAP event.
     * </p>
     * 
     * @param event
     *            event for which the service name is retrieved
     * @return service name
     */
    public static String getServiceNameFromEvent(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return ((SOAPEventType) event.getType()).getServiceName();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getServiceName();
        }
        else {
            throw new RuntimeException(
                                       "Wrong event type. Only SOAPEventType and SimpleSOAPEventType supported but was: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * 
     * <p>
     * Helper function to get the called method from a SOAP event
     * </p>
     * 
     * @param event
     *            event for which the called method is retrieved
     * @return called method
     */
    public static String getCalledMethodFromEvent(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return ((SOAPEventType) event.getType()).getCalledMethod();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getCalledMethod();
        }
        else {
            throw new RuntimeException(
                                       "Wrong event type. Only SOAPEventType and SimpleSOAPEventType supported but was: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * Helper function to get the name of a client from a SOAP event.
     * </p>
     * 
     * @param event
     *            event for which the client name is retrieved
     * @return service name
     */
    public static String getClientNameFromEvent(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return ((SOAPEventType) event.getType()).getClientName();
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getClientName();
        }
        else {
            throw new RuntimeException(
                                       "Wrong event type. Only SOAPEventType and SimpleSOAPEventType supported but was: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * Helper function to get the body of a SOAP request.
     * </p>
     * 
     * @param event
     *            event for which the SOAP request body is retrieved
     * @return body of the SOAP event
     */
    public static Element getSoapBodyFromEvent(Event event) {
        return getSoapBodyFromEvent(event, false, CallType.REQUEST);
    }

    /**
     * <p>
     * Helper function to get the body of a SOAP message.
     * </p>
     * 
     * @param event
     *            event for which the SOAP message body is retrieved
     * @param useRandomBodies
     *            defines if random message bodies are used or the body of the associated event
     * @param callType
     *            defines if the request or response of a message is retrieved
     * @return body of the SOAP event
     */
    public static Element getSoapBodyFromEvent(Event event,
                                               boolean useRandomBodies,
                                               CallType callType)
    {
        if (event.getType() instanceof SOAPEventType) {
            switch (callType)
            {
                case REQUEST:
                    return ((SOAPEventType) event.getType()).getSoapRequestBody();
                case RESPONSE:
                    return ((SOAPEventType) event.getType()).getSoapResponseBody();
                default:
                    throw new RuntimeException("unsupported call type: " + callType);
            }
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            switch (callType)
            {
                case REQUEST:
                    if (((SimpleSOAPEventType) event.getType()).getCallType() == CallType.REQUEST) {
                        if (useRandomBodies) {
                            return ((SimpleSOAPEventType) event.getType()).getRandomSoapMsgBody();
                        }
                        else {
                            return ((SimpleSOAPEventType) event.getType()).getSoapMsgBody();
                        }
                    }
                    else {
                        throw new RuntimeException(
                                                   "cannot retrieve request body, is of CallType: " +
                                                       ((SimpleSOAPEventType) event.getType())
                                                           .getCallType());
                    }
                case RESPONSE:
                    if (((SimpleSOAPEventType) event.getType()).getCallType() == CallType.RESPONSE)
                    {
                        if (useRandomBodies) {
                            return ((SimpleSOAPEventType) event.getType()).getRandomSoapMsgBody();
                        }
                        else {
                            return ((SimpleSOAPEventType) event.getType()).getSoapMsgBody();
                        }
                    }
                    else {
                        throw new RuntimeException(
                                                   "cannot retrieve response body, is of CallType: " +
                                                       ((SimpleSOAPEventType) event.getType())
                                                           .getCallType());
                    }
                default:
                    throw new RuntimeException("unsupported call type: " + callType);
            }
        }
        else {
            throw new RuntimeException(
                                       "unsupported event type; must be SOAPEventType or SimpleSOAPEventType but is: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * Checks if an event is a SOAP request
     * </p>
     * 
     * @param event
     *            event that is checked
     * @return true if SOAP request; false otherwise
     */
    public static boolean isSOAPRequest(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return true;
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getCallType() == CallType.REQUEST;
        }
        else {
            throw new RuntimeException(
                                       "unsupported event type; must be SOAPEventType or SimpleSOAPEventType but is: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * Checks if an event is a SOAP response
     * </p>
     * 
     * @param event
     *            event that is checked
     * @return true if SOAP response; false otherwise
     */
    public static boolean isSOAPResponse(Event event) {
        if (event.getType() instanceof SOAPEventType) {
            return true;
        }
        else if (event.getType() instanceof SimpleSOAPEventType) {
            return ((SimpleSOAPEventType) event.getType()).getCallType() == CallType.RESPONSE;
        }
        else {
            throw new RuntimeException(
                                       "unsupported event type; must be SOAPEventType or SimpleSOAPEventType but is: " +
                                           event.getType().getClass().getName());
        }
    }

    /**
     * <p>
     * returns the XML serialization of a DOM node; located here because it is used for SOAP request
     * bodies
     * </p>
     * 
     * @param node
     *            DOM node that is serialized
     * @return XML serialization as String; null if node is null
     */
    public static String getSerialization(Node node) {
        if (node == null) {
            return null;
        }
        try {
            StringWriter writer = new StringWriter();
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.transform(new DOMSource(node), new StreamResult(writer));
            return writer.toString();
        }
        catch (TransformerFactoryConfigurationError | TransformerException e) {
            throw new IllegalArgumentException(
                                               "could not create serialization for SOAP request body.",
                                               e);
        }
    }

    /**
     * <p>
     * Fetches all {@link Element}s that are direct children of the parent node, whose name matches
     * the given name.
     * </p>
     * 
     * @param typeNameRaw
     *            name of elements that are looked for
     * @param parentNode
     *            DOM node in which the elements are searched for
     * @return found {@link Element}s
     */
    public static List<Element> getMatchingChildNode(String typeNameRaw, Element parentNode) {
        List<Element> matchingNodes = new ArrayList<>();
        Node parameterNode = null;
        if (parentNode != null) {
            NodeList parameterNodes = parentNode.getChildNodes();
            String[] typeNameSplit = typeNameRaw.split(":");
            String typeName = typeNameSplit[typeNameSplit.length - 1];
            for (int i = 0; i < parameterNodes.getLength(); i++) {
                parameterNode = parameterNodes.item(i);
                if (parameterNode.getNodeType() == Node.ELEMENT_NODE) {
                    String[] parameterNodeSplit = parameterNode.getNodeName().split(":");
                    String parameterNodeName = parameterNodeSplit[parameterNodeSplit.length - 1];
                    if (typeName.equals(parameterNodeName)) {
                        matchingNodes.add((Element) parameterNode);
                    }
                }
            }
        }
        return matchingNodes;
    }
    
    /**
     * <p>
     * Fetches the names of all child elements of a node
     * </p>
     *
     * @param parentNode node for which the child names are fetched
     * @return names of the child nodes
     */
    public static List<String> getChildNodeNames(Element parentNode) {
        List<String> childNames = new LinkedList<>();
        Node parameterNode = null;
        if (parentNode != null) {
            NodeList parameterNodes = parentNode.getChildNodes();
            for (int i = 0; i < parameterNodes.getLength(); i++) {
                parameterNode = parameterNodes.item(i);
                if (parameterNode.getNodeType() == Node.ELEMENT_NODE) {
                    String[] parameterNodeSplit = parameterNode.getNodeName().split(":");
                    String parameterNodeName = parameterNodeSplit[parameterNodeSplit.length - 1];
                    childNames.add(parameterNodeName);
                }
            }
        }
        return childNames;
    }
    
    /**
     * <p>
     * Fetches all children of type {@link Element} from a parent node
     * </p>
     *
     * @param parentNode node for which the child elements are fetched
     * @return the child elements
     */
    public static List<Element> getChildElements(Element parentNode) {
        List<Element> childElements = new LinkedList<>();
        Node parameterNode = null;
        if (parentNode != null) {
            NodeList parameterNodes = parentNode.getChildNodes();
            for (int i = 0; i < parameterNodes.getLength(); i++) {
                parameterNode = parameterNodes.item(i);
                if (parameterNode.getNodeType() == Node.ELEMENT_NODE) {
                    childElements.add((Element) parameterNode);
                }
            }
        }
        return childElements;
    }

    /**
     * <p>
     * Returns the values found in a currentNode for a defined valueName. To this aim, this methods
     * first determines if there is an attribute with the name "valueName". If this is the case, the
     * value of this attribute is returned. Otherwise, the methods looks for {@link Element}s that
     * are direct children of the provided DOM node with the given name and returns the text content
     * of those nodes.
     * </p>
     * <p>
     * In case no values can be found, an empty list is returned
     * 
     * @param valueName
     *            name of the value that is retrieved
     * @param node
     *            node for which the value is retrieved
     * @return list of the found values.
     */
    public static List<String> getValuesFromElement(String valueName, Element node) {
        List<String> attributeValues = new LinkedList<>();

        if (node != null) {
            // first check attributes of the node
            Attr attribute = node.getAttributeNode(valueName);
            if (attribute != null) {
                attributeValues.add(attribute.getValue());
            }
            else {
                // now check elements
                List<Element> elements = getMatchingChildNode(valueName, node);
                for (Element element : elements) {
                    attributeValues.add(element.getTextContent());
                }
            }
        }

        return attributeValues;
    }

    /**
     * <p>
     * Allows the removal of pre- and suffixes from SOAP operation names in
     * {@link SimpleSOAPEventType}.
     * </p>
     * 
     * @param sequences
     *            sequences where the operation names are normalized
     */
    public static Collection<List<Event>> normalizeOperationNames(Collection<List<Event>> sequences,
                                                                  String prefixToRemove,
                                                                  String suffixToRemove)
    {
        Collection<List<Event>> normalizedSequences = new LinkedList<>();
        for (List<Event> sequence : sequences) {
            List<Event> normalizedSequence = new LinkedList<>();
            for (Iterator<Event> eventIter = sequence.iterator(); eventIter.hasNext();) {
                Event event = eventIter.next();
                if ((event.getType() instanceof SimpleSOAPEventType)) {
                    SimpleSOAPEventType eventType = (SimpleSOAPEventType) event.getType();
                    String methodName = eventType.getCalledMethod();
                    if (prefixToRemove != null && methodName.startsWith(prefixToRemove)) {
                        methodName =
                            methodName.substring(prefixToRemove.length(), methodName.length());
                        // remove prefix
                    }
                    if (suffixToRemove != null && methodName.endsWith(suffixToRemove)) {
                        methodName =
                            methodName.substring(0, methodName.length() - suffixToRemove.length());
                    }
                    event =
                        new Event(new SimpleSOAPEventType(methodName, eventType.getServiceName(),
                                                          eventType.getClientName(),
                                                          eventType.getSoapMsgBody(),
                                                          eventType.getEqualSOAPDataMap(),
                                                          eventType.getCallType()),
                                  event.getTarget());
                }
                normalizedSequence.add(event);
            }
            normalizedSequences.add(normalizedSequence);
        }
        return normalizedSequences;
    }

    /**
     * <p>
     * Sorts the sequences by the orderingId of the requests/responses. This function only supports
     * the ordering of {@link Event}s with a {@link SOAPEventType}.
     * </p>
     * 
     * @param sequences
     *            sequences to be order
     * @param orderType
     *            determines if sequences are ordered by request or response
     * @return sorted sequences
     */
    public static Collection<List<Event>> sortSequences(Collection<List<Event>> sequences,
                                                        SequenceOrder orderType)
    {
        Collection<List<Event>> sortedSequences = new LinkedList<>();
        for (List<Event> sequence : sequences) {
            // use insertion sort
            List<Event> sortedSequence = new LinkedList<>();
            long lastOrderId = Long.MIN_VALUE;
            long selectedOrderId;
            Event selectedEvent;
            while (sortedSequence.size() != sequence.size()) {
                selectedEvent = null;
                selectedOrderId = Long.MAX_VALUE;
                for (Event event : sequence) {
                    if (!(event.getType() instanceof SOAPEventType)) {
                        throw new RuntimeException(
                                                   "Can only order SOAPEventTypes. SimpleSOAPEvent is also not supported. Event type found: " +
                                                       event.getType().getClass().getName());
                    }
                    SOAPEventType soapEventType = (SOAPEventType) event.getType();
                    long eventOrderId;
                    switch (orderType)
                    {
                        case REQUEST:
                            eventOrderId = soapEventType.getExchange().getRequest().getOrderingId();
                            break;
                        case RESPONSE:
                            eventOrderId =
                                soapEventType.getExchange().getResponse().getOrderingId();
                            break;
                        default:
                            throw new RuntimeException("unsupported order type: " +
                                orderType.toString());
                    }
                    if (eventOrderId > lastOrderId && eventOrderId < selectedOrderId) {
                        selectedOrderId = eventOrderId;
                        selectedEvent = event;
                    }
                }
                if (selectedEvent != null) {
                    sortedSequence.add(selectedEvent);
                    lastOrderId = selectedOrderId;
                }
                else {
                    throw new RuntimeException(
                                               "could not select next event; possibly the same order Id for multiple exchanges events!?");
                }
            }
            sortedSequences.add(sortedSequence);
        }
        return sortedSequences;
    }

    /**
     * <p>
     * Sorts the sequences by the orderingId of the requests/responses. This function only supports
     * the ordering of {@link Event}s with a {@link SOAPEventType}.
     * </p>
     * 
     * @param sequences
     *            sequences to be order
     * @param orderType
     *            determines if sequences are ordered by request or response
     * @return sorted sequences
     */
    public static Collection<List<Event>> sortAndConvertSequences(Collection<List<Event>> sequences,
                                                                  boolean keepRequests,
                                                                  boolean keepResponse)
    {
        Collection<List<Event>> sortedSequences = new LinkedList<>();
        EqualSOAPDataMap equalSOAPDataMap = new EqualSOAPDataMap();
        for (List<Event> sequence : sequences) {
            // use insertion sort
            List<Event> sortedSequence = new LinkedList<>();
            long lastOrderId = Long.MIN_VALUE;
            long selectedOrderId;
            Event selectedEvent;
            CallType selectedCallType = CallType.RESPONSE;
            do {
                selectedEvent = null;
                selectedOrderId = Long.MAX_VALUE;
                for (Event event : sequence) {
                    if (!(event.getType() instanceof SOAPEventType)) {
                        throw new RuntimeException(
                                                   "Can only order SOAPEventTypes. SimpleSOAPEvent is also not supported. Event type found: " +
                                                       event.getType().getClass().getName());
                    }
                    SOAPEventType soapEventType = (SOAPEventType) event.getType();
                    long requestOrderId = soapEventType.getExchange().getRequest().getOrderingId();
                    long responseOrderId =
                        soapEventType.getExchange().getResponse().getOrderingId();
                    // System.out.println(requestOrderId + " / " + responseOrderId);

                    if (requestOrderId > lastOrderId && requestOrderId < selectedOrderId) {
                        selectedOrderId = requestOrderId;
                        selectedEvent = event;
                        selectedCallType = CallType.REQUEST;
                    }
                    if (responseOrderId > lastOrderId && responseOrderId < selectedOrderId) {
                        selectedOrderId = responseOrderId;
                        selectedEvent = event;
                        selectedCallType = CallType.RESPONSE;
                    }
                }
                if (selectedEvent != null) {
                    SOAPEventType eventType = (SOAPEventType) selectedEvent.getType();
                    Element soapMsgBody;
                    switch (selectedCallType)
                    {
                        case REQUEST:
                            soapMsgBody = eventType.getSoapRequestBody();
                            break;
                        case RESPONSE:
                            soapMsgBody = eventType.getSoapResponseBody();
                            break;
                        default:
                            throw new RuntimeException("unsupported call type: " + selectedCallType);
                    }
                    if ((keepRequests && selectedCallType == CallType.REQUEST) ||
                        (keepResponse && selectedCallType == CallType.RESPONSE))
                    {
                        sortedSequence.add(new Event(new SimpleSOAPEventType(eventType
                            .getCalledMethod(), eventType.getServiceName(), eventType
                            .getClientName(), soapMsgBody, equalSOAPDataMap, selectedCallType)));
                    }
                    lastOrderId = selectedOrderId;
                }

            }
            while (selectedEvent != null);
            sortedSequences.add(sortedSequence);
        }
        return sortedSequences;
    }

    /**
     * <p>
     * Removes calls to and from all ignored services from the sequences.
     * </p>
     * 
     * @param sequences
     *            sequences where the ignored services are removed
     * @param ignoredServicesString
     *            comma separted string that defines the ignored services
     * @return sequences without events that reference the ignored services
     */
    public static Collection<List<Event>> removeCallsToIgnoredServices(Collection<List<Event>> sequences,
                                                                       String ignoredServicesString)
    {
        Set<String> ignoredServices = new HashSet<>();
        if (ignoredServicesString != null) {
            for (String service : ignoredServicesString.split(",")) {
                ignoredServices.add(service.trim());
            }
        }
        return removeCallsToIgnoredServices(sequences, ignoredServices);
    }

    /**
     * <p>
     * Removes calls to and from all ignored services from the sequences.
     * </p>
     * 
     * @param sequences
     *            sequences where the ignored services are removed
     * @param ignoredServices
     *            set with all ignored service names
     * @return sequences without events that reference the ignored services
     */
    public static Collection<List<Event>> removeCallsToIgnoredServices(Collection<List<Event>> sequences,
                                                                       Set<String> ignoredServices)
    {
        Collection<List<Event>> onlyAcceptedServicesSequences = new LinkedList<>();
        for (List<Event> sequence : sequences) {
            List<Event> onlyAcceptedServicesSequence = new LinkedList<>();
            for (Event event : sequence) {
                SimpleSOAPEventType eventType = (SimpleSOAPEventType) event.getType();
                if (!ignoredServices.contains(eventType.getServiceName()) &&
                    !ignoredServices.contains(eventType.getClientName()))
                {
                    onlyAcceptedServicesSequence.add(event);
                }
            }
            onlyAcceptedServicesSequences.add(onlyAcceptedServicesSequence);
        }
        return onlyAcceptedServicesSequences;
    }

    /**
     * <p>
     * Drops all requests without responses as well as invalid request/response orders. Only the
     * part of the sequence before the first invalid occurrence is kept.
     * </p>
     * 
     * @param sequences
     *            sequences where the invalid pairs are dropped
     * @return sequences with only valid and complete interactions
     */
    public static Collection<List<Event>> dropInvalidResponseRequestPairs(Collection<List<Event>> sequences)
    {
        Collection<List<Event>> validSequences = new LinkedList<>();
        int i = 0;
        for (List<Event> sequence : sequences) {
            List<Event> validSequence = dropInvalidResponseRequestPairs(sequence);
            if (validSequence.isEmpty() ||
                (validSequence.size() <= 2 && validSequence.get(0) == Event.STARTEVENT))
            {
                Console.traceln(Level.INFO, "dropped sequence " + i +
                    ": empty after removal of invalid request/response pairs");
            }
            validSequences.add(validSequence);
            i++;
        }
        return validSequences;
    }

    /**
     * <p>
     * Drops all requests without responses as well as invalid request/response orders. Only the
     * part of the sequence before the first invalid occurrence is kept.
     * </p>
     * 
     * @param sequence
     *            sequence where the invalid pairs are dropped
     * @return sequence with only valid and complete interactions
     */
    public static List<Event> dropInvalidResponseRequestPairs(List<Event> sequence) {
        Stack<SimpleSOAPEventType> unrespondedRequests = new Stack<>();
        boolean hasStartEvent = false;
        int lastValidIndex = 0;
        int i = 0;
        for (Event event : sequence) {
            if (event == Event.STARTEVENT) {
                hasStartEvent = true;
                lastValidIndex = i;
            }
            else if (event.getType() instanceof SimpleSOAPEventType) {
                SimpleSOAPEventType currentEventType = (SimpleSOAPEventType) event.getType();
                if (SOAPUtils.isSOAPRequest(event)) {
                    unrespondedRequests.push(currentEventType);
                }
                else if (SOAPUtils.isSOAPResponse(event)) {
                    if (unrespondedRequests.empty()) {
                        break; // found a response without previous request; sequence invalid from
                               // here
                    }
                    else {
                        SimpleSOAPEventType lastRequest = unrespondedRequests.peek();
                        if (isRequestResponseMatch(lastRequest, currentEventType)) {
                            unrespondedRequests.pop();
                            if (unrespondedRequests.empty()) {
                                lastValidIndex = i;
                            }
                        }
                    }

                }
            }
            i++;
        }
        List<Event> validSequence = new LinkedList<>(sequence.subList(0, lastValidIndex + 1));
        if (hasStartEvent) {
            validSequence.add(Event.ENDEVENT);
        }
        return validSequence;
    }

    /**
     * <p>
     * Checks if two {@link SimpleSOAPEventType} types are equal except their {@link CallType},
     * which must be {@value CallType#REQUEST} for the first parameter and {@link CallType#RESPONSE}
     * for the second parameter.
     * </p>
     * 
     * @param request
     *            request soap event
     * @param response
     *            response soap event
     * @return true if they are a matching request/response pair
     */
    public static boolean isRequestResponseMatch(SimpleSOAPEventType request,
                                                 SimpleSOAPEventType response)
    {
        if (request == null && response == null) {
            return true;
        }
        if ((request != null && response == null) || (request == null && response != null)) {
            return false;
        }
        return request.getCallType().equals(CallType.REQUEST) &&
            response.getCallType().equals(CallType.RESPONSE) &&
            HTTPUtils.equals(request.getCalledMethod(), response.getCalledMethod()) &&
            HTTPUtils.equals(request.getServiceName(), response.getServiceName()) &&
            HTTPUtils.equals(request.getClientName(), response.getClientName());
    }

    /**
     * <p>
     * prevent instantiation
     * </p>
     */
    private SOAPUtils() {}
}
