ConcurrentStoryReporter.java

package org.jbehave.core.reporters;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.GivenStories;
import org.jbehave.core.model.Lifecycle;
import org.jbehave.core.model.Lifecycle.ExecutionType;
import org.jbehave.core.model.Narrative;
import org.jbehave.core.model.OutcomesTable;
import org.jbehave.core.model.Scenario;
import org.jbehave.core.model.Step;
import org.jbehave.core.model.Story;
import org.jbehave.core.model.StoryDuration;
import org.jbehave.core.steps.StepCollector.Stage;
import org.jbehave.core.steps.StepCreator.PendingStep;
import org.jbehave.core.steps.Timing;

/**
 * When running a multithreading mode, reports cannot be written concurrently but should 
 * be delayed and invoked only at the end of a story, ensuring synchronization on the delegate
 * responsible for the reporting.
 */
public class ConcurrentStoryReporter implements StoryReporter {

    private static Method beforeStoriesSteps;
    private static Method afterStoriesSteps;
    private static Method storyCancelled;
    private static Method storyExcluded;
    private static Method beforeStory;
    private static Method afterStory;
    private static Method narrative;
    private static Method lifecycle;
    private static Method beforeStorySteps;
    private static Method afterStorySteps;
    private static Method beforeScenarioSteps;
    private static Method afterScenarioSteps;
    private static Method beforeComposedSteps;
    private static Method afterComposedSteps;
    private static Method scenarioExcluded;
    private static Method beforeScenarios;
    private static Method beforeScenario;
    private static Method afterScenario;
    private static Method afterScenarios;
    private static Method beforeGivenStories;
    private static Method givenStories;
    private static Method givenStoriesPaths;
    private static Method afterGivenStories;
    private static Method beforeExamples;
    private static Method example;
    private static Method afterExamples;
    private static Method beforeStep;
    private static Method successful;
    private static Method ignorable;
    private static Method comment;
    private static Method pending;
    private static Method pendingDeprecated;
    private static Method notPerformed;
    private static Method failed;
    private static Method failedOutcomes;
    private static Method dryRun;
    private static Method pendingMethods;
    private static Method restarted;
    private static Method restartedStory;

    static {
        try {
            beforeStoriesSteps = StoryReporter.class.getMethod("beforeStoriesSteps", Stage.class);
            afterStoriesSteps = StoryReporter.class.getMethod("afterStoriesSteps", Stage.class);
            storyCancelled = StoryReporter.class.getMethod("storyCancelled", Story.class, StoryDuration.class);
            storyExcluded = StoryReporter.class.getMethod("storyExcluded", Story.class, String.class);
            beforeStory = StoryReporter.class.getMethod("beforeStory", Story.class, Boolean.TYPE);
            afterStory = StoryReporter.class.getMethod("afterStory", Boolean.TYPE);
            narrative = StoryReporter.class.getMethod("narrative", Narrative.class);
            lifecycle = StoryReporter.class.getMethod("lifecycle", Lifecycle.class);
            beforeStorySteps = StoryReporter.class.getMethod("beforeStorySteps", Stage.class, ExecutionType.class);
            afterStorySteps = StoryReporter.class.getMethod("afterStorySteps", Stage.class, ExecutionType.class);
            beforeScenarioSteps = StoryReporter.class.getMethod("beforeScenarioSteps", Stage.class,
                    ExecutionType.class);
            afterScenarioSteps = StoryReporter.class.getMethod("afterScenarioSteps", Stage.class, ExecutionType.class);
            beforeComposedSteps = StoryReporter.class.getMethod("beforeComposedSteps");
            afterComposedSteps = StoryReporter.class.getMethod("afterComposedSteps");
            scenarioExcluded = StoryReporter.class.getMethod("scenarioExcluded", Scenario.class, String.class);
            beforeScenarios = StoryReporter.class.getMethod("beforeScenarios");
            beforeScenario = StoryReporter.class.getMethod("beforeScenario", Scenario.class);
            afterScenario = StoryReporter.class.getMethod("afterScenario", Timing.class);
            afterScenarios = StoryReporter.class.getMethod("afterScenarios");
            beforeGivenStories = StoryReporter.class.getMethod("beforeGivenStories");
            givenStories = StoryReporter.class.getMethod("givenStories", GivenStories.class);
            givenStoriesPaths = StoryReporter.class.getMethod("givenStories", List.class);
            afterGivenStories = StoryReporter.class.getMethod("afterGivenStories");
            beforeExamples = StoryReporter.class.getMethod("beforeExamples", List.class, ExamplesTable.class);
            example = StoryReporter.class.getMethod("example", Map.class, int.class);
            afterExamples = StoryReporter.class.getMethod("afterExamples");
            beforeStep = StoryReporter.class.getMethod("beforeStep", Step.class);
            successful = StoryReporter.class.getMethod("successful", String.class);
            ignorable = StoryReporter.class.getMethod("ignorable", String.class);
            comment = StoryReporter.class.getMethod("comment", String.class);
            pending = StoryReporter.class.getMethod("pending", PendingStep.class);
            pendingDeprecated = StoryReporter.class.getMethod("pending", String.class);
            notPerformed = StoryReporter.class.getMethod("notPerformed", String.class);
            failed = StoryReporter.class.getMethod("failed", String.class, Throwable.class);
            failedOutcomes = StoryReporter.class.getMethod("failedOutcomes", String.class, OutcomesTable.class);
            dryRun = StoryReporter.class.getMethod("dryRun");
            pendingMethods = StoryReporter.class.getMethod("pendingMethods", List.class);
            restarted = StoryReporter.class.getMethod("restarted", String.class, Throwable.class);
            restartedStory = StoryReporter.class.getMethod("restartedStory", Story.class, Throwable.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    private final StoryReporter crossReferencing;
    private final StoryReporter delegate;
    private final StoryReporter threadSafeDelegate;
    private final boolean multiThreading;
    private final List<DelayedMethod> delayedMethods;
    private boolean invoked = false;

    public ConcurrentStoryReporter(StoryReporter crossReferencing, List<StoryReporter> delegates,
            boolean multiThreading) {
        this.crossReferencing = crossReferencing;
        List<StoryReporter> reporters = new ArrayList<>(delegates);
        List<StoryReporter> threadSafeDelegates = reporters.stream()
                                                           .filter(ThreadSafeReporter.class::isInstance)
                                                           .collect(Collectors.toList());
        this.threadSafeDelegate = new DelegatingStoryReporter(threadSafeDelegates);
        reporters.removeAll(threadSafeDelegates);
        this.delegate = new DelegatingStoryReporter(reporters);
        this.multiThreading = multiThreading;
        this.delayedMethods = multiThreading ? Collections.synchronizedList(new ArrayList<DelayedMethod>()) : null;
    }

    @Override
    public void beforeStoriesSteps(Stage stage) {
        perform(reporter ->  reporter.beforeStoriesSteps(stage), beforeStoriesSteps, stage);
    }

    @Override
    public void afterStoriesSteps(Stage stage) {
        perform(reporter ->  reporter.afterStoriesSteps(stage), afterStoriesSteps, stage);
    }

    @Override
    public void storyExcluded(Story story, String filter) {
        perform(reporter -> reporter.storyExcluded(story, filter), storyExcluded, story, filter);
    }

    @Override
    public void beforeStory(Story story, boolean givenStory) {
        perform(reporter ->  reporter.beforeStory(story, givenStory), beforeStory, story, givenStory);
    }

    @Override
    public void afterStory(boolean givenStory) {
        perform(reporter ->  reporter.afterStory(givenStory), afterStory, givenStory);
    }

    @Override
    public void narrative(Narrative narrative) {
        perform(reporter ->  reporter.narrative(narrative), ConcurrentStoryReporter.narrative, narrative);
    }
    
    @Override
    public void lifecycle(Lifecycle lifecycle) {
        perform(reporter ->  reporter.lifecycle(lifecycle), ConcurrentStoryReporter.lifecycle, lifecycle);
    }

    @Override
    public void beforeStorySteps(Stage stage, Lifecycle.ExecutionType type) {
        perform(reporter ->  reporter.beforeStorySteps(stage, type), beforeStorySteps, stage, type);
    }

    @Override
    public void afterStorySteps(Stage stage, Lifecycle.ExecutionType type) {
        perform(reporter ->  reporter.afterStorySteps(stage, type), afterStorySteps, stage, type);
    }

    @Override
    public void beforeComposedSteps() {
        perform(StoryReporter::beforeComposedSteps, beforeComposedSteps);
    }

    @Override
    public void afterComposedSteps() {
        perform(StoryReporter::afterComposedSteps, afterComposedSteps);
    }

    @Override
    public void beforeScenarioSteps(Stage stage, Lifecycle.ExecutionType type) {
        perform(reporter ->  reporter.beforeScenarioSteps(stage, type), beforeScenarioSteps, stage, type);
    }

    @Override
    public void afterScenarioSteps(Stage stage, Lifecycle.ExecutionType type) {
        perform(reporter ->  reporter.afterScenarioSteps(stage, type), afterScenarioSteps, stage, type);
    }

    @Override
    public void scenarioExcluded(Scenario scenario, String filter) {
        perform(reporter ->  reporter.scenarioExcluded(scenario, filter), scenarioExcluded, scenario, filter);
    }

    @Override
    public void beforeScenarios() {
        perform(StoryReporter::beforeScenarios, beforeScenarios);
    }

    @Override
    public void beforeScenario(Scenario scenario) {
        perform(reporter ->  reporter.beforeScenario(scenario), beforeScenario, scenario);
    }

    @Override
    public void afterScenario(Timing timing) {
        perform(reporter -> reporter.afterScenario(timing), afterScenario, timing);
    }

    @Override
    public void afterScenarios() {
        perform(StoryReporter::afterScenarios, afterScenarios);
    }

    @Override
    public void beforeGivenStories() {
        perform(StoryReporter::beforeGivenStories, beforeGivenStories);
    }

    @Override
    public void givenStories(GivenStories stories) {
        perform(reporter ->  reporter.givenStories(stories), givenStories, stories);
    }

    @Override
    public void givenStories(List<String> storyPaths) {
        perform(reporter ->  reporter.givenStories(storyPaths), givenStoriesPaths, storyPaths);
    }

    @Override
    public void afterGivenStories() {
        perform(StoryReporter::afterGivenStories, afterGivenStories);
    }

    @Override
    public void beforeExamples(List<String> steps, ExamplesTable table) {
        perform(reporter ->  reporter.beforeExamples(steps, table), beforeExamples, steps, table);
    }

    @Override
    public void example(Map<String, String> tableRow, int exampleIndex) {
        perform(reporter ->  reporter.example(tableRow, exampleIndex), example, tableRow, exampleIndex);
    }

    @Override
    public void afterExamples() {
        perform(StoryReporter::afterExamples, afterExamples);
    }

    @Override
    public void beforeStep(Step step) {
        perform(reporter ->  reporter.beforeStep(step), beforeStep, step);
    }

    @Override
    public void successful(String step) {
        perform(reporter ->  reporter.successful(step), successful, step);
    }

    @Override
    public void ignorable(String step) {
        perform(reporter ->  reporter.ignorable(step), ignorable, step);
    }

    @Override
    public void comment(String step) {
        perform(reporter ->  reporter.comment(step), comment, step);
    }

    @Override
    public void pending(PendingStep step) {
        perform(reporter ->  reporter.pending(step), pending, step);
    }

    @Override
    public void pending(String step) {
        perform(reporter ->  reporter.pending(step), pendingDeprecated, step);
    }

    @Override
    public void notPerformed(String step) {
        perform(reporter ->  reporter.notPerformed(step), notPerformed, step);
    }

    @Override
    public void failed(String step, Throwable cause) {
        perform(reporter ->  reporter.failed(step, cause), failed, step, cause);
    }

    @Override
    public void failedOutcomes(String step, OutcomesTable table) {
        perform(reporter ->  reporter.failedOutcomes(step, table), failedOutcomes, step, table);
    }

    @Override
    public void dryRun() {
        perform(StoryReporter::dryRun, dryRun);
    }

    @Override
    public void pendingMethods(List<String> methods) {
        perform(reporter ->  reporter.pendingMethods(methods), pendingMethods, methods);
    }
    
    @Override
    public void restarted(String step, Throwable cause) {
        perform(reporter ->  reporter.restarted(step, cause), restarted, step, cause);
    }
    
    @Override
    public void restartedStory(Story story, Throwable cause) {
        perform(reporter ->  reporter.restartedStory(story, cause), restartedStory, story, cause);
    }

    @Override
    public void storyCancelled(Story story, StoryDuration storyDuration) {
        perform(reporter ->  reporter.storyCancelled(story, storyDuration), storyCancelled, story, storyDuration);
    }

    private void perform(Consumer<StoryReporter> crossReferencingInvoker, Method delayedMethod,
            Object... delayedMethodArgs) {
        crossReferencingInvoker.accept(crossReferencing);
        crossReferencingInvoker.accept(threadSafeDelegate);
        if (multiThreading) {
            delayedMethods.add(new DelayedMethod(delayedMethod, delayedMethodArgs));
        } else {
            crossReferencingInvoker.accept(delegate);
        }
    }

    public StoryReporter getDelegate() {
        return delegate;
    }

    public boolean invoked() {
        return invoked;
    }
    
    public void invokeDelayed() {
        if (!multiThreading) {
            return;
        }
        synchronized (delegate) {
            for (DelayedMethod delayed : delayedMethods) {
                delayed.invoke(delegate);
            }
            delayedMethods.clear();
        }
        invoked = true;
    }

    public static class DelayedMethod {
        private Method method;
        private Object[] args;

        public DelayedMethod(Method method, Object... args) {
            this.method = method;
            this.args = args;
        }

        public void invoke(StoryReporter delegate) {
            try {
                method.invoke(delegate, args);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException("" + method, e);
            }
        }
    }
}