org.jumpmind.metl.core.runtime.flow.StepRuntime.java Source code

Java tutorial

Introduction

Here is the source code for org.jumpmind.metl.core.runtime.flow.StepRuntime.java

Source

/**
 * Licensed to JumpMind Inc under one or more contributor
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU General Public License, version 3.0 (GPLv3)
 * (the "License"); you may not use this file except in compliance
 * with the License.
 *
 * You should have received a copy of the GNU General Public License,
 * version 3.0 (GPLv3) along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 * 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 org.jumpmind.metl.core.runtime.flow;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.jumpmind.metl.core.model.Component;
import org.jumpmind.metl.core.model.FlowStep;
import org.jumpmind.metl.core.runtime.BinaryMessage;
import org.jumpmind.metl.core.runtime.ContentMessage;
import org.jumpmind.metl.core.runtime.ControlMessage;
import org.jumpmind.metl.core.runtime.EntityData;
import org.jumpmind.metl.core.runtime.EntityDataMessage;
import org.jumpmind.metl.core.runtime.LogLevel;
import org.jumpmind.metl.core.runtime.Message;
import org.jumpmind.metl.core.runtime.MessageHeader;
import org.jumpmind.metl.core.runtime.MisconfiguredException;
import org.jumpmind.metl.core.runtime.ShutdownMessage;
import org.jumpmind.metl.core.runtime.TextMessage;
import org.jumpmind.metl.core.runtime.component.AbstractComponentRuntime;
import org.jumpmind.metl.core.runtime.component.AssertException;
import org.jumpmind.metl.core.runtime.component.ComponentContext;
import org.jumpmind.metl.core.runtime.component.ComponentStatistics;
import org.jumpmind.metl.core.runtime.component.IComponentRuntime;
import org.jumpmind.metl.core.runtime.component.IComponentRuntimeFactory;
import org.jumpmind.metl.core.runtime.component.definition.IComponentDefinitionFactory;
import org.jumpmind.metl.core.runtime.component.definition.XMLComponent;
import org.jumpmind.metl.core.runtime.resource.IResourceFactory;
import org.jumpmind.metl.core.util.LogUtils;
import org.jumpmind.metl.core.util.ThreadUtils;
import org.jumpmind.util.AppUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StepRuntime implements Runnable {

    protected final Logger log = LoggerFactory.getLogger(getClass());

    public static final String THREAD_COUNT = "thread.count";

    public static final String UNIT_OF_WORK_INPUT_MESSAGE = "Input Message";

    public static final String UNIT_OF_WORK_FLOW = "Flow";

    protected BlockingQueue<Message> inQueue;

    protected Executor componentRuntimeExecutor;

    boolean running = false;

    boolean cancelled = false;

    boolean finished = false;

    Throwable error;

    List<StepRuntime> targetStepRuntimes;

    List<StepRuntime> sourceStepRuntimes;

    Map<String, Boolean> sourceStepRuntimeUnitOfWorkReceived;

    Set<String> targetStepRuntimeUnitOfWorkSent;

    ComponentContext componentContext;

    IComponentRuntimeFactory componentRuntimeFactory;

    IComponentDefinitionFactory componentDefintionFactory;

    XMLComponent componentDefintion;

    FlowRuntime flowRuntime;

    Map<Integer, IComponentRuntime> componentRuntimeByThread = new HashMap<>();

    Boolean startStep = null;

    Set<String> liveSourceStepIds;

    int contentMessagesSentCount;

    int controlMessagesSentCount;

    int activeCount;

    int queueCapacity;

    int threadCount;

    public StepRuntime(IComponentRuntimeFactory componentFactory,
            IComponentDefinitionFactory componentDefinitionFactory, ComponentContext componentContext,
            FlowRuntime flowRuntime) {
        this.flowRuntime = flowRuntime;
        this.componentContext = componentContext;
        this.queueCapacity = componentContext.getFlowStep().getComponent()
                .getInt(AbstractComponentRuntime.INBOUND_QUEUE_CAPACITY, 1000);
        this.inQueue = new LinkedBlockingQueue<Message>(queueCapacity);
        this.sourceStepRuntimeUnitOfWorkReceived = new HashMap<String, Boolean>();
        this.targetStepRuntimeUnitOfWorkSent = new HashSet<String>();
        this.componentRuntimeFactory = componentFactory;
        this.componentDefintionFactory = componentDefinitionFactory;
        this.componentDefintion = componentDefintionFactory.getDefinition(getComponentType());
    }

    private String getComponentType() {
        return componentContext.getFlowStep().getComponent().getType();
    }

    public int getContentMessagesSentCount() {
        return contentMessagesSentCount;
    }

    public int getControlMessagesSentCount() {
        return controlMessagesSentCount;
    }

    public boolean isQueueEmpty() {
        Message message = this.inQueue.peek();
        return message == null || message instanceof ShutdownMessage;
    }

    public void setTargetStepRuntimes(List<StepRuntime> targetStepRuntimes) {
        this.targetStepRuntimes = targetStepRuntimes;
    }

    public List<StepRuntime> getTargetStepRuntimes() {
        return targetStepRuntimes;
    }

    public void setSourceStepRuntimes(List<StepRuntime> sourceStepRuntimes) {
        this.sourceStepRuntimes = sourceStepRuntimes;
        this.liveSourceStepIds = new HashSet<>();
        for (StepRuntime stepRuntime : sourceStepRuntimes) {
            this.liveSourceStepIds.add(stepRuntime.getComponentContext().getFlowStep().getId());
        }
    }

    public List<StepRuntime> getSourceStepRuntimes() {
        return sourceStepRuntimes;
    }

    protected void queue(Message message) throws InterruptedException {
        if (inQueue.remainingCapacity() == 0 && message.getHeader().getOriginatingStepId()
                .equalsIgnoreCase(componentContext.getFlowStep().getId())) {
            throw new RuntimeException("Inbound queue capacity on " + componentContext.getFlowStep().getName()
                    + " not sufficient to handle inbound messages from other components in addition to inbound messages from itself.");
        }
        inQueue.put(message);
    }

    public void start(IResourceFactory resourceFactory) {
        try {
            componentContext.setComponentStatistics(new ComponentStatistics());
            startStep = sourceStepRuntimes == null || sourceStepRuntimes.size() == 0;
            Component component = componentContext.getFlowStep().getComponent();
            threadCount = component.getInt(StepRuntime.THREAD_COUNT, 1);
            if (threadCount > 1) {
                String prefix = String.format("%s-%s", LogUtils.normalizeName(flowRuntime.getAgent().getName()),
                        LogUtils.normalizeName(componentContext.getFlowStep().getName()));
                this.componentRuntimeExecutor = ThreadUtils.createFixedThreadPool(prefix, queueCapacity,
                        threadCount);
            }
            for (int threadNumber = 1; threadNumber <= threadCount; threadNumber++) {
                createComponentRuntime(threadNumber);
            }
        } catch (RuntimeException ex) {
            recordError(1, ex);
            throw ex;
        }
    }

    protected void createComponentRuntime(int threadNumber) {
        String type = getComponentType();
        IComponentRuntime componentRuntime = componentRuntimeFactory.create(type);
        componentRuntimeByThread.put(threadNumber, componentRuntime);
        if (sourceStepRuntimes.size() == 0 && !componentRuntime.supportsStartupMessages()) {
            throw new MisconfiguredException("%s must have an inbound connection from another component",
                    componentRuntime.getComponentDefintion().getName());
        } else {
            componentContext.getExecutionTracker().flowStepStarted(threadNumber, componentContext);
            componentRuntime.start(threadNumber, componentContext);
        }
    }

    protected void recordError(int threadNumber, Throwable ex) {
        String msg = null;
        if (ex instanceof MisconfiguredException || ex instanceof AssertException) {
            msg = ex.getMessage();
        } else {
            msg = ExceptionUtils.getFullStackTrace(ex);
        }

        componentContext.getExecutionTracker().log(threadNumber, LogLevel.ERROR, componentContext, msg);

        /*
         * First time through here
         */
        if (error == null) {
            error = ex;
            flowRuntime.cancel();
        }
    }

    protected SendMessageCallback createSendMessageCallback() {
        return new SendMessageCallback();
    }

    @Override
    public void run() {
        try {
            SendMessageCallback target = createSendMessageCallback();
            /*
             * If we are a start step (don't have any input links), we'll only
             * get a single message which is the start message sent by the flow
             * runtime to kick things off. If we have input links, we must loop
             * until we get a shutdown message from one of our sources
             */
            while (running && !cancelled) {
                /*
                 * Continue to poll as long as the flow is running. other
                 * components could be generating messages which could block if
                 * we don't continue to poll
                 */
                Message inputMessage = null;
                synchronized (this) {
                    inputMessage = inQueue.poll();
                    if (inputMessage != null && !(inputMessage instanceof ShutdownMessage)) {
                        activeCount++;
                    }
                }
                if (running && !cancelled) {
                    if (inputMessage != null) {
                        if (inputMessage instanceof ShutdownMessage) {
                            process((ShutdownMessage) inputMessage, target);
                        } else {
                            process(inputMessage, target);
                        }
                    } else {
                        AppUtils.sleep(50);
                    }
                }
            }
        } catch (Exception ex) {
            recordError(1, ex);
        }
    }

    protected synchronized void decrementActiveCount() {
        activeCount--;
    }

    protected synchronized int getActiveCountPlusQueueSize() {
        return activeCount + inQueue.size();
    }

    protected void process(Message inputMessage, SendMessageCallback target) {
        boolean unitOfWorkBoundaryReached = calculateUnitOfWorkLastMessage(inputMessage);
        /*
         * If unitOfWorkBoundaryReached, we might want to consider waiting to
         * send this until all other threads have finished processing to avoid
         * race conditions.
         */
        if (threadCount > 1) {
            this.componentRuntimeExecutor
                    .execute(() -> processOnAnotherThread(inputMessage, unitOfWorkBoundaryReached, target));
        } else {
            processOnAnotherThread(inputMessage, unitOfWorkBoundaryReached, target);
        }
    }

    protected void processOnAnotherThread(Message inputMessage, boolean unitOfWorkBoundaryReached,
            SendMessageCallback target) {
        int threadNumber = ThreadUtils.getThreadNumber(threadCount);
        try {
            componentContext.getComponentStatistics().incrementInboundMessages(threadNumber);
            if (inputMessage instanceof ContentMessage<?>) {
                Object payload = ((ContentMessage<?>) inputMessage).getPayload();
                if (payload instanceof Collection<?>) {
                    componentContext.getComponentStatistics().incrementNumberInboundPayload(threadNumber,
                            ((Collection<?>) payload).size());
                } else if (payload != null) {
                    componentContext.getComponentStatistics().incrementNumberInboundPayload(threadNumber);
                }
            }

            componentContext.getExecutionTracker().beforeHandle(threadNumber, componentContext);

            IComponentRuntime componentRuntime = componentRuntimeByThread.get(threadNumber);

            Component component = componentContext.getFlowStep().getComponent();
            boolean logInput = component.getBoolean(AbstractComponentRuntime.LOG_INPUT, false);

            if (logInput) {
                logInput(inputMessage, target, unitOfWorkBoundaryReached);
            }
            target.setCurrentInputMessage(threadNumber, inputMessage);
            componentRuntime.handle(inputMessage, target, unitOfWorkBoundaryReached);

            boolean recursionDone = liveSourceStepIds.size() == 1
                    && liveSourceStepIds.contains(componentContext.getFlowStep().getId())
                    && getActiveCountPlusQueueSize() == 1;

            if ((unitOfWorkBoundaryReached || recursionDone)
                    && componentRuntime.getComponentDefintion().isAutoSendControlMessages()) {
                verifyAndSendControlMessageToTargets(target, inputMessage);
            }

            /*
             * Detect shutdown condition
             */
            if (startStep || recursionDone) {
                shutdown(threadNumber, target, false);
            }
        } catch (Exception ex) {
            recordError(ThreadUtils.getThreadNumber(threadCount), ex);
        } finally {
            componentContext.getExecutionTracker().afterHandle(threadNumber, componentContext, error);
            decrementActiveCount();
        }
    }

    protected synchronized boolean idle() {
        return activeCount <= 0;
    }

    protected void process(ShutdownMessage shutdownMessage, SendMessageCallback target) {
        cancelled = shutdownMessage.isCancelled();

        if (log.isDebugEnabled()) {
            log.debug("Processing shutdown message for " + componentContext.getFlowStep().getName()
                    + (cancelled ? ". The status was cancelled" : ""));
        }

        String fromStepId = shutdownMessage.getHeader().getOriginatingStepId();
        liveSourceStepIds.remove(fromStepId);

        /*
         * When all of the source step runtimes have been removed or when the
         * shutdown message comes from myself, then go ahead and shutdown
         */
        if (cancelled || fromStepId == null || liveSourceStepIds == null || liveSourceStepIds.size() == 0
                || fromStepId.equals(componentContext.getFlowStep().getId())) {
            shutdown(1, target, true);
        }
    }

    private void verifyAndSendControlMessageToTargets(ISendMessageCallback target, Message inputMessage) {
        for (StepRuntime targetRuntime : targetStepRuntimes) {
            if (!targetStepRuntimeUnitOfWorkSent
                    .contains(targetRuntime.getComponentContext().getFlowStep().getId())) {
                log.info("Automatically sending a last unit of work message from "
                        + componentContext.getFlowStep().getComponent().getName() + " to "
                        + targetRuntime.getComponentContext().getFlowStep().getComponent().getName()
                        + " because one was received but not sent forward.");
                target.sendControlMessage(inputMessage.getHeader());
            }
        }
    }

    protected boolean calculateUnitOfWorkLastMessage(Message inputMessage) {
        boolean lastMessage = true;
        if (inputMessage instanceof ControlMessage) {
            sourceStepRuntimeUnitOfWorkReceived.put(inputMessage.getHeader().getOriginatingStepId(), Boolean.TRUE);
        }
        Set<StepRuntime> activeRuntimes = new HashSet<>();
        for (StepRuntime sourceRuntime : sourceStepRuntimes) {
            boolean controlMessageReceived = sourceStepRuntimeUnitOfWorkReceived
                    .get(sourceRuntime.getComponentContext().getFlowStep().getId()) != null;
            lastMessage &= controlMessageReceived;
            if (!controlMessageReceived) {
                activeRuntimes.add(sourceRuntime);
            }
        }

        if (!lastMessage) {
            lastMessage = areStepRuntimesDead(activeRuntimes);
        }

        if (lastMessage) {
            // TODO figure out when/how to reset the last unit of work calc
            // sourceStepRuntimeUnitOfWorkReceived.clear();
        }
        return lastMessage;
    }

    protected boolean areStepRuntimesDead(Set<StepRuntime> runtimes) {
        for (StepRuntime stepRuntime : runtimes) {
            if (!isStepRuntimeDead(stepRuntime)) {
                return false;
            }
        }
        return true;
    }

    protected boolean isStepRuntimeDead(StepRuntime stepRuntime) {
        if (stepRuntime.getContentMessagesSentCount() > 0 || stepRuntime.getControlMessagesSentCount() > 0
                || !stepRuntime.isQueueEmpty() || !stepRuntime.idle()) {
            return false;
        } else {
            List<StepRuntime> parentSteps = stepRuntime.getSourceStepRuntimes();
            for (StepRuntime parentStep : parentSteps) {
                boolean parentStepIsDead = parentStep.isStepRuntimeDead(parentStep);
                if (!parentStepIsDead && parentStep.getControlMessagesSentCount() == 0) {
                    return false;
                }
            }
        }
        return true;
    }

    private void stop(int threadNumber, IComponentRuntime componentRuntime) {
        try {
            componentRuntime.stop();
        } catch (Exception e) {
            recordError(threadNumber, e);
        }
    }

    private void shutdownTargets(StepRuntime targetStepRuntime) {
        try {
            targetStepRuntime.queue(new ShutdownMessage(componentContext.getFlowStep().getId(), cancelled));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void shutdown(int threadNumber, ISendMessageCallback target, boolean waitForShutdown) {
        shutdownThreads(waitForShutdown);

        if (log.isDebugEnabled()) {
            log.debug("Shutting down " + componentContext.getFlowStep().getName());
        }

        targetStepRuntimes.forEach(t -> shutdownTargets(t));
        componentRuntimeByThread.values().forEach(c -> stop(threadNumber, c));

        finished = true;
        running = false;

        recordFlowStepFinished();
    }

    private void shutdownThreads(boolean waitForShutdown) {
        if (this.componentRuntimeExecutor instanceof ExecutorService) {
            try {
                ExecutorService service = (ExecutorService) this.componentRuntimeExecutor;
                service.shutdown();
                while (waitForShutdown && !service.isTerminated()) {
                    service.awaitTermination(500, TimeUnit.MILLISECONDS);
                }
            } catch (Exception e) {
                recordError(1, e);
            }
        }
    }

    private final void recordFlowStepFinished() {
        componentRuntimeByThread.keySet().forEach(e -> componentContext.getExecutionTracker().flowStepFinished(e,
                componentContext, error, cancelled));
    }

    public void cancel() {
        shutdownThreads(true);
        if (!finished) {
            this.cancelled = true;
            recordFlowStepFinished();
        }
    }

    public void startRunning() {
        this.running = true;
    }

    public boolean isRunning() {
        return running;
    }

    public List<IComponentRuntime> getComponentRuntimes() {
        return new ArrayList<>(componentRuntimeByThread.values());
    }

    public void flowCompletedWithoutError() {
        componentRuntimeByThread.values().forEach(c -> flowCompletedWithoutError(c));
    }

    private void flowCompletedWithoutError(IComponentRuntime componentRuntime) {
        if (!cancelled) {
            try {
                componentRuntime.flowCompleted(cancelled);
            } catch (Exception ex) {
                recordError(1, ex);
                componentContext.getExecutionTracker().flowStepFailedOnComplete(componentContext, ex);
            }
        }
    }

    public void flowCompletedWithErrors(Throwable myError, List<Throwable> allErrors) {
        componentRuntimeByThread.values().forEach(c -> flowCompletedWithErrors(c, myError, allErrors));
    }

    private void flowCompletedWithErrors(IComponentRuntime componentRuntime, Throwable myError,
            List<Throwable> allErrors) {
        if (!cancelled) {
            try {
                componentRuntime.flowCompletedWithErrors(myError);
            } catch (Exception ex) {
                recordError(1, ex);
                componentContext.getExecutionTracker().flowStepFailedOnComplete(componentContext, ex);
            }
        }
    }

    public Throwable getError() {
        return error;
    }

    public ComponentContext getComponentContext() {
        return componentContext;
    }

    protected void logInput(Message inputMessage, ISendMessageCallback messageTarget,
            boolean unitOfWorkBoundaryReached) {
        MessageHeader header = inputMessage.getHeader();

        int threadNumber = ThreadUtils.getThreadNumber(threadCount);

        String source = "ENTRY";
        try {
            source = componentContext.getManipulatedFlow().findFlowStepWithId(header.getOriginatingStepId())
                    .getName();
        } catch (NullPointerException e) {
            // Do nothing allow "ENTRY" as the source.
        }

        componentContext.getExecutionTracker().log(threadNumber, LogLevel.INFO, componentContext,
                String.format("INPUT %s{sequenceNumber=%d,unitOfWorkBoundaryReached=%s,source='%s',headers=%s}",
                        inputMessage.getClass().getSimpleName(), header.getSequenceNumber(),
                        unitOfWorkBoundaryReached, source, header));
        if (inputMessage instanceof EntityDataMessage) {
            EntityDataMessage message = (EntityDataMessage) inputMessage;
            if (componentContext.getFlowStep().getComponent().getInputModel() != null) {
                ArrayList<EntityData> payload = message.getPayload();
                for (EntityData entityData : payload) {
                    componentContext.getExecutionTracker().log(threadNumber, LogLevel.INFO, componentContext,
                            String.format("INPUT Message Payload: %s",
                                    componentContext.getFlowStep().getComponent().toRow(entityData, true, true)));
                }
            }
        } else if (inputMessage instanceof TextMessage) {
            TextMessage message = (TextMessage) inputMessage;
            ArrayList<String> payload = message.getPayload();
            for (String string : payload) {
                componentContext.getExecutionTracker().log(threadNumber, LogLevel.INFO, componentContext,
                        String.format("INPUT Message Payload: %s", string));
            }
        }

    }

    protected void logOutput(Message outputMessage, String... targetFlowStepIds) {

        String targets = targetFlowStepIds != null && targetFlowStepIds.length > 0
                ? Arrays.toString(targetFlowStepIds)
                : "[all]";
        int threadNumber = ThreadUtils.getThreadNumber(threadCount);

        MessageHeader header = outputMessage.getHeader();
        componentContext.getExecutionTracker().log(threadNumber, LogLevel.INFO, componentContext,
                String.format("OUTPUT %s{sequenceNumber=%d,headers=%s,targetsteps:%s}",
                        outputMessage.getClass().getSimpleName(), header.getSequenceNumber(), header, targets));
        if (outputMessage instanceof EntityDataMessage) {
            EntityDataMessage message = (EntityDataMessage) outputMessage;
            if (componentContext.getFlowStep().getComponent().getOutputModel() != null) {
                ArrayList<EntityData> payload = message.getPayload();
                for (EntityData entityData : payload) {
                    componentContext.getExecutionTracker().log(threadNumber, LogLevel.INFO, componentContext,
                            String.format("OUTPUT Message Payload: %s",
                                    componentContext.getFlowStep().getComponent().toRow(entityData, true, false)));
                }
            }
        } else if (outputMessage instanceof TextMessage) {
            TextMessage message = (TextMessage) outputMessage;
            ArrayList<String> payload = message.getPayload();
            for (String string : payload) {
                componentContext.getExecutionTracker().log(threadNumber, LogLevel.INFO, componentContext,
                        String.format("OUTPUT Message Payload: %s", string));
            }
        }
    }

    class SendMessageCallback implements ISendMessageCallback {

        Map<Integer, Message> currentInputMessages = new HashMap<>();

        private void setCurrentInputMessage(int threadNumber, Message currentInputMessage) {
            currentInputMessages.put(threadNumber, currentInputMessage);
        }

        private Message createMessage(Message newMessage) {
            return createMessage(newMessage, null);
        }

        private Message createMessage(Message newMessage, Map<String, Serializable> headerSettings) {
            ComponentStatistics statistics = componentContext.getComponentStatistics();
            MessageHeader header = newMessage.getHeader();
            Message inputMessage = currentInputMessages.get(ThreadUtils.getThreadNumber(threadCount));
            if (inputMessage != null) {
                header.putAll(inputMessage.getHeader());
            }
            if (headerSettings != null) {
                header.putAll(headerSettings);
            }
            header.setSequenceNumber(
                    statistics.getNumberOutboundMessages(ThreadUtils.getThreadNumber(threadCount)));
            return newMessage;
        }

        @SuppressWarnings("unchecked")
        private <T extends Serializable> T copy(T payload) {
            if (payload instanceof ArrayList) {
                ArrayList<?> old = (ArrayList<?>) payload;
                ArrayList<Object> copied = new ArrayList<>(old.size());
                for (Object object : old) {
                    if (object instanceof EntityData) {
                        object = ((EntityData) object).copy();
                    }
                    copied.add(object);
                }
                payload = (T) copied;
            } else if (payload instanceof byte[]) {
                payload = (T) ArrayUtils.clone((byte[]) payload);
            }
            return payload;
        }

        private void sendMessage(Message message, String... targetFlowStepIds) {
            ComponentStatistics statistics = componentContext.getComponentStatistics();
            int threadNumber = ThreadUtils.getThreadNumber(threadCount);
            statistics.incrementOutboundMessages(threadNumber);
            if (message instanceof ContentMessage<?>) {
                Object payload = ((ContentMessage<?>) message).getPayload();
                if (payload instanceof Collection<?>) {
                    componentContext.getComponentStatistics().incrementNumberOutboundPayload(threadNumber,
                            ((Collection<?>) payload).size());
                } else if (payload != null) {
                    componentContext.getComponentStatistics().incrementNumberOutboundPayload(threadNumber);
                }
            }

            componentContext.getExecutionTracker().updateStatistics(threadNumber, componentContext);

            Component component = componentContext.getFlowStep().getComponent();
            boolean logOutput = component.getBoolean(AbstractComponentRuntime.LOG_OUTPUT, false);

            if (logOutput) {
                logOutput(message, targetFlowStepIds);
            }

            Collection<String> targetStepIds = targetFlowStepIds != null ? Arrays.asList(targetFlowStepIds)
                    : Collections.emptyList();

            for (StepRuntime targetRuntime : targetStepRuntimes) {
                boolean forward = targetStepIds == null || targetStepIds.size() == 0
                        || targetStepIds.contains(targetRuntime.getComponentContext().getFlowStep().getId());
                if (forward) {
                    try {
                        if (log.isDebugEnabled()) {
                            log.debug("Sending " + message.getClass().getSimpleName() + " to "
                                    + targetRuntime.getComponentContext().getFlowStep().getName());
                        }
                        targetRuntime.queue(message);
                        if (message instanceof ControlMessage) {
                            targetStepRuntimeUnitOfWorkSent
                                    .add(targetRuntime.getComponentContext().getFlowStep().getId());
                        }
                    } catch (Exception e) {
                        if (e instanceof RuntimeException) {
                            throw (RuntimeException) e;
                        } else {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }

        private void validateEntityData(ArrayList<EntityData> payload) {
            for (EntityData entityData : payload) {
                if (entityData.size() == 0) {
                    throw new MisconfiguredException("The component attempted to send an empty entity record.");
                }
            }
        }

        @Override
        public void sendShutdownMessage(boolean cancel) {
            FlowStep flowStep = componentContext.getFlowStep();
            sendMessage(createMessage(new ShutdownMessage(flowStep.getId(), cancel)));
        }

        @Override
        public void sendControlMessage() {
            sendControlMessage(null);
        }

        @Override
        public void sendControlMessage(Map<String, Serializable> messageHeaders, String... targetStepIds) {
            FlowStep flowStep = componentContext.getFlowStep();
            sendMessage(createMessage(new ControlMessage(flowStep.getId()), messageHeaders), targetStepIds);
            controlMessagesSentCount++;
        }

        @Override
        public void sendBinaryMessage(Map<String, Serializable> messageHeaders, byte[] payload,
                String... targetStepIds) {
            payload = copy(payload);
            FlowStep flowStep = componentContext.getFlowStep();
            sendMessage(createMessage(new BinaryMessage(flowStep.getId(), payload), messageHeaders), targetStepIds);
            contentMessagesSentCount++;
        }

        @Override
        public void sendEntityDataMessage(Map<String, Serializable> messageHeaders, ArrayList<EntityData> payload,
                String... targetStepIds) {
            validateEntityData(payload);
            payload = copy(payload);
            FlowStep flowStep = componentContext.getFlowStep();
            sendMessage(createMessage(new EntityDataMessage(flowStep.getId(), payload), messageHeaders),
                    targetStepIds);
            contentMessagesSentCount++;
        }

        @Override
        public void sendTextMessage(Map<String, Serializable> messageHeaders, String payload,
                String... targetStepIds) {
            ArrayList<String> messages = new ArrayList<>();
            messages.add(payload);
            sendTextMessage(messageHeaders, messages, targetStepIds);
        }

        @Override
        public void sendTextMessage(Map<String, Serializable> messageHeaders, ArrayList<String> payload,
                String... targetStepIds) {
            payload = copy(payload);
            FlowStep flowStep = componentContext.getFlowStep();
            sendMessage(createMessage(new TextMessage(flowStep.getId(), payload), messageHeaders), targetStepIds);
            contentMessagesSentCount++;
        }

        @Override
        public void forward(Map<String, Serializable> messageHeaders, Message message) {
            if (message instanceof EntityDataMessage) {
                sendEntityDataMessage(messageHeaders, ((EntityDataMessage) message).getPayload());
            } else if (message instanceof TextMessage) {
                sendTextMessage(messageHeaders, ((TextMessage) message).getPayload());
            } else if (message instanceof BinaryMessage) {
                sendBinaryMessage(messageHeaders, ((BinaryMessage) message).getPayload());
            }
        }

        @Override
        public void forward(Message message) {
            forward(null, message);
        }
    }

    @Override
    public String toString() {
        return componentContext.getFlowStep().getName();
    }

}