org.cleverbus.core.AbstractOperationRouteTest.java Source code

Java tutorial

Introduction

Here is the source code for org.cleverbus.core.AbstractOperationRouteTest.java

Source

/*
 * Copyright (C) 2015
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.cleverbus.core;

import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertThat;

import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Nullable;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.namespace.QName;

import org.cleverbus.api.asynch.AsynchConstants;
import org.cleverbus.api.asynch.model.TraceHeader;
import org.cleverbus.api.asynch.model.TraceIdentifier;
import org.cleverbus.api.entity.ExternalSystemExtEnum;
import org.cleverbus.api.entity.Message;
import org.cleverbus.api.entity.MsgStateEnum;
import org.cleverbus.api.entity.ServiceExtEnum;
import org.cleverbus.api.route.AbstractBasicRoute;
import org.cleverbus.core.common.asynch.AsynchMessageRoute;
import org.cleverbus.core.common.asynch.TraceHeaderProcessor;
import org.cleverbus.core.common.dao.MessageDao;
import org.cleverbus.test.ActiveRoutes;

import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.Processor;
import org.apache.camel.Produce;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.StringSource;
import org.apache.camel.builder.AdviceWithRouteBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.model.RouteDefinition;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.kubek2k.springockito.annotations.SpringockitoContextLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;

/**
 * Helper abstract parent for assisted testing of operation routes. This helper provides:
 * <ul>
 * <li>ability to easily convert JAXB POJO requests to XML requests, and vice-versa for responses</li>
 * <li>sender method to send XML payload to async IN route (as if it's a new incoming message from WS)</li>
 * <li>sender method to send XML payload to async OUT route (as if pumping persisted message from DB)</li>
 * <li>resend method to resent the last sent message to async OUT route (e.g., to resend party failed messages)</li>
 * <li>mock URI method for mocking URIs of unstarted routes (to mock WS uris without actually starting WS out routes)</li>
 * </ul>
 */
@ActiveRoutes(classes = { AsynchMessageRoute.class })
@ContextConfiguration(loader = SpringockitoContextLoader.class)
public abstract class AbstractOperationRouteTest extends AbstractCoreDbTest {

    public static final String URI_ASYNC_IN_ROUTE = "direct:testAsyncInRoute";
    public static final String URI_SYNC_ROUTE = "direct:testSyncRoute";

    @Autowired
    protected MessageDao msgDao;

    @Produce
    protected ProducerTemplate producer;

    protected Message lastMessage;

    @Override
    @After
    public void printEntities() {
        // override to add @After
        super.printEntities();
    }

    /**
     * @param testRequest the request JAXB POJO
     * @return a valid request XML that will be used to test the route
     */
    protected String getRequestXML(Object testRequest) throws Exception {
        return marshalFragment(testRequest, null);
    }

    /**
     * @param testRequest      the request JAXB POJO
     * @param testRequestQName the request QName (in case JAXB POJO is not annotated with {@link XmlRootElement})
     * @return a valid request XML that will be used to test the route
     */
    protected String getRequestXML(Object testRequest, QName testRequestQName) throws JAXBException {
        return marshalFragment(testRequest, testRequestQName);
    }

    /**
     * @return the external system that normally calls the operation route to be tested
     */
    protected abstract ExternalSystemExtEnum getSourceSystem();

    /**
     * @return the service of the operation route under test
     */
    protected abstract ServiceExtEnum getService();

    /**
     * @return the operation name of the operation route under test
     */
    protected abstract String getOperationName();

    /**
     * The route ID that acts as the IN input to the asynchronous operation,
     * responsible for delivering synchronous validation response to the caller,
     * but not response for actually executing the asynchronous operation,
     * e.g., what is returned by {@link AbstractBasicRoute#getInRouteId(ServiceExtEnum, String)}.
     *
     * @return the async IN route id.
     */
    protected String getAsyncInRouteId() {
        return AbstractBasicRoute.getInRouteId(getService(), getOperationName());
    }

    /**
     * The route ID that acts as the OUT input to the asynchronous operation,
     * responsible for actually executing the asynchronous operation,
     * but not responsible for delivering synchronous response to the caller.
     * The asynchronous operation success is reported via confirmation mechanism instead.
     * e.g., what is returned by {@link AbstractBasicRoute#getInRouteId(ServiceExtEnum, String)}.
     *
     * @return the async IN route id.
     */
    protected String getAsyncOutRouteId() {
        return AbstractBasicRoute.getOutRouteId(getService(), getOperationName());
    }

    /**
     * The route ID that acts as the input/output to the synchronous operation,
     * e.g., what is returned by {@link AbstractBasicRoute#getRouteId(ServiceExtEnum, String)}.
     *
     * @return the sync route id.
     */
    protected String getSyncRouteId() {
        return AbstractBasicRoute.getRouteId(getService(), getOperationName());
    }

    @Before
    public void connectProducers() throws Exception {
        boolean sync = replaceFrom(getSyncRouteId(), URI_SYNC_ROUTE);
        boolean async = replaceFrom(getAsyncInRouteId(), URI_ASYNC_IN_ROUTE);
        if (!sync && !async) {
            throw new IllegalArgumentException(String.format(
                    "Neither Sync, nor Async route ID is known based on Service %s and Operation Name %s"
                            + " - didn't find route with ID %s or %s",
                    getService(), getOperationName(), getSyncRouteId(), getAsyncInRouteId()));
        }
    }

    @Before
    public void redirectAsyncRoute() throws Exception {
        getCamelContext().addRoutes(new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                from(AsynchConstants.URI_ASYNC_MSG).routeId(AsynchMessageRoute.ROUTE_ID_ASYNC)
                        .log(LoggingLevel.WARN, "Ignoring Message: ${body}");
            }
        });
    }

    /**
     * Sends the test request to the Sync route (as if it was received via Spring WS).
     *
     * @param requestXML the request payload (XML) to send
     * @return the result as an exchange with getOut() containing the response message
     * @throws Exception
     */
    protected Exchange sendSyncMessage(final String requestXML) throws Exception {
        return producer.request(URI_SYNC_ROUTE, new Processor() {
            @Override
            public void process(Exchange exchange) throws Exception {
                exchange.getIn().setBody(requestXML);
            }
        });
    }

    /**
     * Sends the test request to the Sync route (as if it was received via Spring WS).
     *
     * @param requestXML    the request payload (XML) to send
     * @param responseClass {@link String}.class to get the response body as String,
     *                      or the class to unmarshal the response body to using JAXB
     * @return the result as the specified class
     * @throws Exception
     */
    protected <T> T sendSyncMessage(String requestXML, Class<T> responseClass) throws Exception {
        Exchange result = sendSyncMessage(requestXML);
        Exception exception = result.getException();
        if (exception != null) {
            throw exception;
        }
        String responseXML = result.getOut().getMandatoryBody(String.class);
        if (responseClass.isAssignableFrom(String.class)) {
            return responseClass.cast(responseXML);
        }
        return unmarshalFragment(responseXML, responseClass);
    }

    /**
     * Sends the test request to the IN route (as if it was received via Spring WS).
     *
     * @param requestXML    the request payload (XML) to send
     * @param finalState    the final state of the {@link Message} created in the DB - for automatic verification
     * @param responseClass the response class to parse response XML as
     * @return the response parsed from XML as the specified responseClass
     * @throws Exception
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected <T> T sendAsyncInMessage(String requestXML, MsgStateEnum finalState, Class<T> responseClass)
            throws Exception {
        String correlationID = UUID.randomUUID().toString();
        return sendAsyncInMessage(correlationID, DateTime.now(), requestXML, getMessageStateVerifier(finalState),
                responseClass);
    }

    /**
     * Sends the test request to the IN route (as if it was received via Spring WS).
     *
     * @param requestXML      the request payload (XML) to send
     * @param messageVerifier the processor that can verify the {@link Message} created in the DB
     * @param responseClass   the response class to parse response XML as
     * @return the response parsed from XML as the specified responseClass
     * @throws Exception
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected <T> T sendAsyncInMessage(String requestXML, MessageProcessor messageVerifier, Class<T> responseClass)
            throws Exception {
        String correlationID = UUID.randomUUID().toString();
        return sendAsyncInMessage(correlationID, DateTime.now(), requestXML, messageVerifier, responseClass);
    }

    /**
     * Sends the test request to the IN route (as if it was received via Spring WS).
     *
     * @param correlationID   the new message correlation ID
     * @param msgTimestamp    the new message timestamp
     * @param requestXML      the request payload (XML) to send
     * @param messageVerifier the processor that can verify the {@link Message} created in the DB
     * @param responseClass   the response class to parse response XML as
     * @return the response parsed from XML as the specified responseClass
     * @throws Exception
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected <T> T sendAsyncInMessage(String correlationID, DateTime msgTimestamp, String requestXML,
            MessageProcessor messageVerifier, Class<T> responseClass) throws Exception {
        Exchange result = sendAsyncInMessage(correlationID, msgTimestamp, requestXML);
        Exception exception = result.getException();
        if (exception != null) {
            throw exception;
        }

        verifyMessage(getSourceSystem(), correlationID, messageVerifier);
        String responseXML = result.getOut().getMandatoryBody(String.class);
        if (responseClass.isAssignableFrom(String.class)) {
            return responseClass.cast(responseXML);
        }
        return unmarshalFragment(responseXML, responseClass);
    }

    /**
     * Sends the test request to the IN route
     *
     * @param requestXML    the request payload (XML) to send
     * @param responseClass the response class to parse response XML as
     * @return the response parsed from XML as the specified responseClass
     */
    protected <T> T sendAsyncInMessage(String requestXML, Class<T> responseClass) throws Exception {
        return sendAsyncInMessage(requestXML, (MessageProcessor) null, responseClass);
    }

    /**
     * Sends the test request to the IN route
     *
     * @param correlationID the new message correlation ID
     * @param msgTimestamp  the new message timestamp
     * @param requestXML    the request payload (XML) to send
     * @param responseClass the response class to parse response XML as
     * @return the response parsed from XML as the specified responseClass
     */
    protected <T> T sendAsyncInMessage(String correlationID, DateTime msgTimestamp, String requestXML,
            Class<T> responseClass) throws Exception {
        return sendAsyncInMessage(correlationID, msgTimestamp, requestXML, getMessageStateVerifier(null),
                responseClass);
    }

    /**
     * Sends the test request to the IN route.
     *
     * @return the result as an exchange with getOut() containing the output message
     */
    protected Exchange sendAsyncInMessage(final String correlationID, final DateTime timestamp,
            final String payload) {
        Exchange result = producer.request(URI_ASYNC_IN_ROUTE, new Processor() {
            @Override
            public void process(Exchange exchange) throws Exception {
                exchange.getIn().setBody(payload);
                exchange.getIn().setHeaders(createTraceHeader(correlationID, timestamp));
            }
        });
        lastMessage = msgDao.findByCorrelationId(correlationID, getSourceSystem());
        return result;
    }

    /**
     * Sends a new message with the test request to the OUT route,
     * similarly to what {@link org.cleverbus.core.common.asynch.queue.MessagePollExecutor} does.
     *
     * @param requestXML the request payload (XML) to send
     * @param finalState the final state of the {@link Message} created in the DB - for automatic verification
     * @return the {@link Message} that was sent and processed
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected Message sendAsyncOutMessage(String requestXML, MsgStateEnum finalState) throws Exception {
        lastMessage = createAndSaveMessage(requestXML);
        producer.requestBody(AsynchMessageRoute.URI_SYNC_MSG, lastMessage);
        verifyMessage(lastMessage.getSourceSystem(), lastMessage.getCorrelationId(),
                getMessageStateVerifier(finalState));
        return lastMessage;
    }

    /**
     * Sends a new message with the test request to the OUT route,
     * similarly to what {@link org.cleverbus.core.common.asynch.queue.MessagePollExecutor} does.
     *
     * @param correlationID the new message correlation ID
     * @param msgTimestamp  the new message timestamp
     * @param requestXML    the request payload (XML) to send
     * @param finalState    the final state of the {@link Message} created in the DB - for automatic verification
     * @return the {@link Message} that was sent and processed
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected Message sendAsyncOutMessage(final String correlationID, final DateTime msgTimestamp,
            final String requestXML, MsgStateEnum finalState) throws Exception {
        return sendAsyncOutMessage(new MessageProcessor() {
            @Override
            public void process(Message message) {
                message.setMsgTimestamp(msgTimestamp.toDate());
                message.setCorrelationId(correlationID);
                message.setPayload(requestXML);
            }
        }, finalState);
    }

    /**
     * Sends a new message with the test request to the OUT route,
     * similarly to what {@link org.cleverbus.core.common.asynch.queue.MessagePollExecutor} does.
     *
     * @param initializer the message initializer that will set message fields as necessary
     * @return the {@link Message} that was sent and processed
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected Message sendAsyncOutMessage(MessageProcessor initializer) throws Exception {
        return sendAsyncOutMessage(initializer, null);
    }

    /**
     * Sends a new message with the test request to the OUT route,
     * similarly to what {@link org.cleverbus.core.common.asynch.queue.MessagePollExecutor} does.
     *
     * @param initializer the message initializer that will set message fields as necessary
     * @param finalState  the final state of the {@link Message} created in the DB - for automatic verification
     * @return the {@link Message} that was sent and processed
     * @throws AssertionError if the message state doesn't match the specified state
     */
    protected Message sendAsyncOutMessage(final MessageProcessor initializer, MsgStateEnum finalState)
            throws Exception {
        lastMessage = createAndSaveMessage(new MessageProcessor() {
            @Override
            public void process(Message message) throws Exception {
                message.setSourceSystem(getSourceSystem());
                message.setService(getService());
                message.setOperationName(getOperationName());
                message.setMsgTimestamp(DateTime.now().toDate());
                message.setReceiveTimestamp(DateTime.now().toDate());
                message.setCorrelationId(UUID.randomUUID().toString());
                initializer.process(message); // let the provided initializer init the other fields
            }
        });
        producer.requestBody(AsynchMessageRoute.URI_SYNC_MSG, lastMessage);
        verifyMessage(lastMessage.getSourceSystem(), lastMessage.getCorrelationId(),
                getMessageStateVerifier(finalState));
        return lastMessage;
    }

    /**
     * Sends a new message with the test request to the OUT route,
     * similarly to what {@link org.cleverbus.core.common.asynch.queue.MessagePollExecutor} does.
     *
     * @param requestXML the request payload (XML) to send
     * @return the {@link Message} that was sent and processed
     */
    protected Message sendAsyncOutMessage(String requestXML) throws Exception {
        return sendAsyncOutMessage(requestXML, null);
    }

    /**
     * Sends a new message with the test request to the OUT route,
     * similarly to what {@link org.cleverbus.core.common.asynch.queue.MessagePollExecutor} does.
     *
     * @param correlationID the new message correlation ID
     * @param msgTimestamp  the new message timestamp
     * @param requestXML    the request payload (XML) to send
     * @return the {@link Message} that was sent and processed
     */
    protected Message sendAsyncOutMessage(String correlationID, DateTime msgTimestamp, String requestXML)
            throws Exception {
        return sendAsyncOutMessage(correlationID, msgTimestamp, requestXML, null);
    }

    /**
     * Re-sends the last sent message, first setting it's state to {@link MsgStateEnum#PROCESSING}.
     *
     * @return the last {@link Message}, after it was re-sent and re-processed
     */
    protected Message resendAsyncOutMessage(MsgStateEnum finalState) throws Exception {
        return resendAsyncOutMessage(lastMessage, finalState);
    }

    /**
     * Re-sends the specified message, first setting it's state to {@link MsgStateEnum#PROCESSING}.
     *
     * @return the same {@link Message}, after it was re-sent and re-processed
     */
    protected Message resendAsyncOutMessage(Message msg, MsgStateEnum finalState) throws Exception {
        msg.setState(MsgStateEnum.PROCESSING);
        producer.requestBody(AsynchMessageRoute.URI_SYNC_MSG, msg);
        verifyMessage(msg.getSourceSystem(), msg.getCorrelationId(), getMessageStateVerifier(finalState));
        return msg;
    }

    /**
     * Re-sends the last sent message, first setting it's state to {@link MsgStateEnum#PROCESSING}
     *
     * @return the last {@link Message}, after it was re-sent and re-processed
     */
    protected Message resendAsyncOutMessage() throws Exception {
        return resendAsyncOutMessage(lastMessage, null);
    }

    /**
     * Re-sends the specified message, first setting it's state to {@link MsgStateEnum#PROCESSING}
     *
     * @return the same {@link Message}, after it was re-sent and re-processed
     */
    protected Message resendAsyncOutMessage(Message msg) throws Exception {
        return resendAsyncOutMessage(msg, null);
    }

    @Nullable
    protected MessageProcessor getMessageStateVerifier(@Nullable final MsgStateEnum finalState) {
        return finalState == null ? null : new MessageProcessor() {
            @Override
            public void process(Message message) throws Exception {
                String stateFailReason = String.format(
                        "Message doesn't have the expected state. failedErrorCode=%s, failedErrorDesc=%s, businessError=%s",
                        message.getFailedErrorCode(), message.getFailedDesc(), message.getBusinessError());
                assertThat(stateFailReason, message.getState(), is(finalState));
            }
        };
    }

    private void verifyMessage(ExternalSystemExtEnum sourceSystem, String correlationID,
            MessageProcessor msgVerifier) throws Exception {
        if (msgVerifier != null) {
            Message message = msgDao.findByCorrelationId(correlationID, sourceSystem);

            String msgMissingReason = String.format("No message found for sourceSystem=%s and correlationID=%s",
                    sourceSystem, correlationID);

            assertThat(msgMissingReason, message, notNullValue());
            msgVerifier.process(message);
        }
    }

    private Map<String, Object> createTraceHeader(String correlationID, DateTime timestamp) {
        TraceIdentifier traceId = new TraceIdentifier();
        traceId.setCorrelationID(correlationID);
        traceId.setApplicationID(getApplicationID());
        traceId.setTimestamp(timestamp);

        TraceHeader traceHeader = new TraceHeader();
        traceHeader.setTraceIdentifier(traceId);

        final Map<String, Object> headers = new HashMap<String, Object>();
        headers.put(TraceHeaderProcessor.TRACE_HEADER, traceHeader);
        return headers;
    }

    protected Message createAndSaveMessage(String requestXML) throws Exception {
        return createAndSaveMessage(getSourceSystem(), getService(), getOperationName(), requestXML);
    }

    protected Message createAndSaveMessage(MessageProcessor initializer) throws Exception {
        return createAndSaveMessages(1, initializer)[0];
    }

    protected Message createMessage(String requestXML) {
        return createMessage(getSourceSystem(), getService(), getOperationName(), requestXML);
    }

    public Map<String, Object> createHeaders(String correlationID, String applicationID, DateTime timestamp) {
        TraceIdentifier traceId = new TraceIdentifier();
        traceId.setCorrelationID(correlationID);
        traceId.setApplicationID(applicationID);
        traceId.setTimestamp(timestamp);

        TraceHeader traceHeader = new TraceHeader();
        traceHeader.setTraceIdentifier(traceId);

        HashMap<String, Object> headers = new HashMap<String, Object>();
        headers.put(TraceHeaderProcessor.TRACE_HEADER, getTraceHeader());
        return headers;
    }

    /**
     * Gets the applicationID that corresponds to {@link #getSourceSystem()}.
     */
    protected String getApplicationID() {
        return getSourceSystem().getSystemName();
    }

    protected <T> T unmarshalFragment(String responseXML, Class<T> fragmentClass) throws JAXBException {
        Unmarshaller unmarshaller = JAXBContext.newInstance(fragmentClass).createUnmarshaller();
        JAXBElement<T> jaxbElement = unmarshaller.unmarshal(new StringSource(responseXML), fragmentClass);
        return jaxbElement.getValue();
    }

    protected <T> String marshalFragment(T request, QName qName) throws JAXBException {
        StringWriter stringWriter = new StringWriter();
        Marshaller marshaller = JAXBContext.newInstance(request.getClass()).createMarshaller();
        Object element = request;
        if (qName != null) {
            element = new JAXBElement<T>(qName, (Class<T>) request.getClass(), request);
        }
        marshaller.marshal(element, stringWriter);
        return stringWriter.toString();
    }

    private boolean replaceFrom(String routeId, final String uri) throws Exception {
        RouteDefinition routeDefinition = getCamelContext().getRouteDefinition(routeId);
        if (routeDefinition != null) {
            routeDefinition.adviceWith(getCamelContext(), new AdviceWithRouteBuilder() {
                @Override
                public void configure() throws Exception {
                    replaceFromWith(uri);
                }
            });
        }
        return routeDefinition != null;
    }

    /**
     * Mocks a hand-over-type endpoint (direct, direct-vm, seda or vm)
     * by simply providing the other (consumer=From) side connected to a mock.
     * <p/>
     * There should be no consumer existing, i.e., the consumer route should not be started.
     *
     * @param uri the URI a new mock should consume from
     * @return the mock that is newly consuming from the URI
     */
    protected MockEndpoint mockDirect(final String uri) throws Exception {
        return mockDirect(uri, null);
    }

    /**
     * Same as {@link #mockDirect(String)}, except with route ID to be able to override an existing route with the mock.
     *
     * @param uri     the URI a new mock should consume from
     * @param routeId the route ID for the new mock route
     *                (existing route with this ID will be overridden by this new route)
     * @return the mock that is newly consuming from the URI
     */
    protected MockEndpoint mockDirect(final String uri, final String routeId) throws Exception {
        // precaution: check that URI can be mocked by just providing the other side:
        Assert.assertThat(uri,
                anyOf(startsWith("direct:"), startsWith("direct-vm:"), startsWith("seda:"), startsWith("vm:")));
        // create the mock:
        final MockEndpoint createCtidMock = getCamelContext().getEndpoint("mock:" + uri, MockEndpoint.class);
        // redirect output to this mock:
        getCamelContext().addRoutes(new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                RouteDefinition routeDef = from(uri);

                if (routeId != null) {
                    routeDef.routeId(routeId);
                }

                routeDef.to(createCtidMock);
            }
        });
        return createCtidMock;
    }
}