/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.functional.functional;
import static org.apache.commons.lang.SystemUtils.LINE_SEPARATOR;
import static org.mule.functional.functional.FunctionalTestNotification.EVENT_RECEIVED;
import static org.mule.runtime.core.api.Event.getCurrentEvent;
import org.mule.functional.exceptions.FunctionalTestException;
import org.mule.runtime.api.exception.MuleException;
import org.mule.runtime.api.lifecycle.Disposable;
import org.mule.runtime.api.lifecycle.Initialisable;
import org.mule.runtime.api.lifecycle.Startable;
import org.mule.runtime.api.lifecycle.Stoppable;
import org.mule.runtime.api.message.Message;
import org.mule.runtime.api.meta.AbstractAnnotatedObject;
import org.mule.runtime.core.DefaultMuleEventContext;
import org.mule.runtime.core.api.Event;
import org.mule.runtime.core.api.MuleContext;
import org.mule.runtime.core.api.MuleEventContext;
import org.mule.runtime.core.api.construct.FlowConstruct;
import org.mule.runtime.core.api.construct.FlowConstructAware;
import org.mule.runtime.core.api.context.MuleContextAware;
import org.mule.runtime.core.api.lifecycle.Callable;
import org.mule.runtime.core.util.ClassUtils;
import org.mule.runtime.core.util.NumberUtils;
import org.mule.runtime.core.util.StringMessageUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>FunctionalTestComponent</code> is a service that can be used by functional tests. This service accepts an EventCallback
* that can be used to assert the state of the current event.
* <p/>
* Also, this service fires {@link FunctionalTestNotification} via Mule for every message received. Tests can register with Mule
* to receive these events by implementing {@link FunctionalTestNotificationListener}.
*
* @see EventCallback
* @see FunctionalTestNotification
* @see FunctionalTestNotificationListener
*/
// TODO This should really extend StaticComponent from mule-core as it is quite similar.
public class FunctionalTestComponent extends AbstractAnnotatedObject
implements Callable, Initialisable, Disposable, MuleContextAware, FlowConstructAware, Receiveable, Startable, Stoppable {
protected transient Logger logger = LoggerFactory.getLogger(getClass());
public static final int STREAM_SAMPLE_SIZE = 4;
public static final int STREAM_BUFFER_SIZE = 4096;
private EventCallback eventCallback;
private Object returnData = null;
private boolean throwException = false;
private Class<? extends Throwable> exceptionToThrow;
private String exceptionText = "";
private boolean enableMessageHistory = true;
private boolean enableNotifications = true;
private boolean doInboundTransform = true;
private String appendString;
private long waitTime = 0;
private boolean logMessageDetails = false;
private String id = "<none>";
private MuleContext muleContext;
private FlowConstruct flowConstruct;
private static List<LifecycleCallback> lifecycleCallbacks = new ArrayList<>();
/**
* Keeps a list of any messages received on this service. Note that only references to the messages (objects) are stored, so any
* subsequent changes to the objects will change the history.
*/
private List<Object> messageHistory;
@Override
public void initialise() {
if (enableMessageHistory) {
messageHistory = new CopyOnWriteArrayList<>();
}
for (LifecycleCallback callback : lifecycleCallbacks) {
callback.onTransition(id, Initialisable.PHASE_NAME);
}
}
@Override
public void start() throws MuleException {
for (LifecycleCallback callback : lifecycleCallbacks) {
callback.onTransition(id, Startable.PHASE_NAME);
}
}
@Override
public void setMuleContext(MuleContext context) {
this.muleContext = context;
}
@Override
public void setFlowConstruct(FlowConstruct flowConstruct) {
this.flowConstruct = flowConstruct;
}
@Override
public void stop() throws MuleException {
for (LifecycleCallback callback : lifecycleCallbacks) {
callback.onTransition(id, Stoppable.PHASE_NAME);
}
}
@Override
public void dispose() {
for (LifecycleCallback callback : lifecycleCallbacks) {
callback.onTransition(id, Disposable.PHASE_NAME);
}
}
/**
* {@inheritDoc}
*/
@Override
public Object onCall(MuleEventContext context) throws Exception {
if (isThrowException()) {
throwException();
}
return process(getMessageFromContext(context), context);
}
private Object getMessageFromContext(MuleEventContext context) throws MuleException {
if (isDoInboundTransform()) {
Object o = context.getMessage().getPayload().getValue();
if (getAppendString() != null && !(o instanceof String)) {
o = context.transformMessageToString(muleContext);
}
return o;
} else if (getAppendString() != null) {
return context.getMessageAsString(muleContext);
} else {
return context.getMessage().getPayload().getValue();
}
}
/**
* This method is used by some WebServices tests where you don' want to be introducing the
* {@link org.mule.runtime.core.api.MuleEventContext} as a complex type.
*
* @param data the event data received
* @return the processed message
* @throws Exception
*/
@Override
public Object onReceive(Object data) throws Exception {
MuleEventContext context = new DefaultMuleEventContext(flowConstruct, getCurrentEvent());
if (isThrowException()) {
throwException();
}
return process(data, context);
}
/**
* Always throws a {@link FunctionalTestException}. This methodis only called if {@link #isThrowException()} is true.
*
* @throws FunctionalTestException or the exception specified in 'exceptionType
*/
protected void throwException() throws Exception {
if (getExceptionToThrow() != null) {
if (StringUtils.isNotBlank(exceptionText)) {
Throwable exception = ClassUtils.instanciateClass(getExceptionToThrow(),
new Object[] {exceptionText});
throw (Exception) exception;
} else {
throw (Exception) getExceptionToThrow().newInstance();
}
} else {
if (StringUtils.isNotBlank(exceptionText)) {
throw new FunctionalTestException(exceptionText);
} else {
throw new FunctionalTestException();
}
}
}
/**
* Will append the value of {@link #getAppendString()} to the contents of the message. This has a side affect that the inbound
* message will be converted to a string and the return payload will be a string. Note that the value of
* {@link #getAppendString()} can contain expressions.
*
* @param contents the string vlaue of the current message payload
* @param event the current event
* @return a concatenated string of the current payload and the appendString
*/
protected String append(String contents, Event event) {
return contents + muleContext.getExpressionManager().parse(appendString, event, flowConstruct);
}
/**
* The service method that implements the test component logic. This method can be called publically through either
* {@link #onCall(org.mule.runtime.core.api.MuleEventContext)} or {@link #onReceive(Object)}
*
* @param data The message payload
* @param context the current {@link org.mule.runtime.core.api.MuleEventContext}
* @return a new message payload according to the configuration of the component
* @throws Exception if there is a general failure or if {@link #isThrowException()} is true.
*/
protected Object process(Object data, MuleEventContext context) throws Exception {
// System.out.println(data + " at " + new java.util.Date());
if (enableMessageHistory) {
messageHistory.add(data);
}
if (logger.isInfoEnabled()) {
String msg = StringMessageUtils.getBoilerPlate("Message Received in service: "
+ context.getFlowConstruct().getName() + ". Content is: "
+ StringMessageUtils.truncate(data.toString(), 100, true), '*', 80);
logger.info(msg);
}
final Message message = context.getMessage();
if (isLogMessageDetails() && logger.isInfoEnabled()) {
StringBuilder sb = new StringBuilder();
sb.append("Full Message: ").append(LINE_SEPARATOR);
sb.append(message.getPayload().getValue().toString()).append(LINE_SEPARATOR);
sb.append(message.toString());
logger.info(sb.toString());
}
if (eventCallback != null) {
eventCallback.eventReceived(context, this, muleContext);
}
Object replyMessage;
if (returnData != null) {
if (returnData instanceof String && muleContext.getExpressionManager().isExpression(returnData.toString())) {
replyMessage = muleContext.getExpressionManager().parse(returnData.toString(), context.getEvent(), flowConstruct);
} else {
replyMessage = returnData;
}
} else {
if (appendString != null) {
replyMessage = append(data.toString(), context.getEvent());
} else {
replyMessage = data;
}
}
if (isEnableNotifications()) {
muleContext.fireNotification(new FunctionalTestNotification(context, replyMessage, EVENT_RECEIVED));
}
// Time to wait before returning
if (waitTime > 0) {
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
logger.info("FunctionalTestComponent waitTime was interrupted");
}
}
return replyMessage;
}
/**
* An event callback is called when a message is received by the service. An MuleEvent callback isn't strictly required but it
* is usfal for performing assertions on the current message being received. Note that the FunctionalTestComponent should be
* made a singleton when using MuleEvent callbacks
* <p/>
* Another option is to register a {@link FunctionalTestNotificationListener} with Mule and this will deleiver a
* {@link FunctionalTestNotification} for every message received by this service
*
* @return the callback to call when a message is received
* @see FunctionalTestNotification
* @see FunctionalTestNotificationListener
*/
public EventCallback getEventCallback() {
return eventCallback;
}
/**
* An event callback is called when a message is received by the service. An MuleEvent callback isn't strictly required but it
* is usfal for performing assertions on the current message being received. Note that the FunctionalTestComponent should be
* made a singleton when using MuleEvent callbacks
* <p/>
* Another option is to register a {@link FunctionalTestNotificationListener} with Mule and this will deleiver a
* {@link FunctionalTestNotification} for every message received by this service
*
* @param eventCallback the callback to call when a message is received
* @see FunctionalTestNotification
* @see FunctionalTestNotificationListener
*/
public void setEventCallback(EventCallback eventCallback) {
this.eventCallback = eventCallback;
}
/**
* Often you will may want to return a fixed message payload to simulate and external system call. This can be done using the
* 'returnData' property. Note that you can return complex objects by using the <container-property> element in the Xml
* configuration.
*
* @return the message payload to always return from this service instance
*/
public Object getReturnData() {
return returnData;
}
/**
* Often you will may want to return a fixed message payload to simulate and external system call. This can be done using the
* 'returnData' property. Note that you can return complex objects by using the <container-property> element in the Xml
* configuration.
*
* @param returnData the message payload to always return from this service instance
*/
public void setReturnData(Object returnData) {
this.returnData = returnData;
}
/**
* Sometimes you will want the service to always throw an exception, if this is the case you can set the 'throwException'
* property to true.
*
* @return throwException true if an exception should always be thrown from this instance. If the {@link #returnData} property
* is set and is of type java.lang.Exception, that exception will be thrown.
*/
public boolean isThrowException() {
return throwException;
}
/**
* Sometimes you will want the service to always throw an exception, if this is the case you can set the 'throwException'
* property to true.
*
* @param throwException true if an exception should always be thrown from this instance. If the {@link #returnData} property is
* set and is of type java.lang.Exception, that exception will be thrown.
*/
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
public boolean isEnableMessageHistory() {
return enableMessageHistory;
}
public void setEnableMessageHistory(boolean enableMessageHistory) {
this.enableMessageHistory = enableMessageHistory;
}
/**
* If enableMessageHistory = true, returns the number of messages received by this service.
*
* @return -1 if no message history, otherwise the history size
*/
public int getReceivedMessagesCount() {
if (messageHistory != null) {
return messageHistory.size();
} else {
return NumberUtils.INTEGER_MINUS_ONE.intValue();
}
}
/**
* If enableMessageHistory = true, returns a message received by the service in chronological order. For example,
* getReceivedMessage(1) returns the first message received by the service, getReceivedMessage(2) returns the second message
* received by the service, etc.
*/
public Object getReceivedMessage(int number) {
Object message = null;
if (messageHistory != null) {
if (number <= messageHistory.size()) {
message = messageHistory.get(number - 1);
}
}
return message;
}
/**
* If enableMessageHistory = true, returns the last message received by the service in chronological order.
*/
public Object getLastReceivedMessage() {
if (messageHistory != null) {
return messageHistory.get(messageHistory.size() - 1);
} else {
return null;
}
}
public String getAppendString() {
return appendString;
}
public void setAppendString(String appendString) {
this.appendString = appendString;
}
public boolean isEnableNotifications() {
return enableNotifications;
}
public void setEnableNotifications(boolean enableNotifications) {
this.enableNotifications = enableNotifications;
}
public Class<? extends Throwable> getExceptionToThrow() {
return exceptionToThrow;
}
public void setExceptionToThrow(Class<? extends Throwable> exceptionToThrow) {
this.exceptionToThrow = exceptionToThrow;
}
public long getWaitTime() {
return waitTime;
}
public void setWaitTime(long waitTime) {
this.waitTime = waitTime;
}
public boolean isDoInboundTransform() {
return doInboundTransform;
}
public void setDoInboundTransform(boolean doInboundTransform) {
this.doInboundTransform = doInboundTransform;
}
public boolean isLogMessageDetails() {
return logMessageDetails;
}
public void setLogMessageDetails(boolean logMessageDetails) {
this.logMessageDetails = logMessageDetails;
}
public String getExceptionText() {
return exceptionText;
}
public void setExceptionText(String text) {
exceptionText = text;
}
public void setId(String id) {
this.id = id;
}
public static void addLifecycleCallback(LifecycleCallback callback) {
lifecycleCallbacks.add(callback);
}
public static void removeLifecycleCallback(LifecycleCallback callback) {
lifecycleCallbacks.remove(callback);
}
public interface LifecycleCallback {
void onTransition(String name, String newPhase);
}
}