org.age.util.fsm.StateMachineServiceBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.age.util.fsm.StateMachineServiceBuilder.java

Source

/*
 * Copyright (C) 2014 Intelligent Information Systems Group.
 *
 * This file is part of AgE.
 *
 * AgE 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.
 *
 * AgE 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 AgE.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 * Created: 2012-08-21
 * $Id: 53954ddb2f07014defed684e02246fe0ee1a1afa $
 */

package org.age.util.fsm;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.newEnumMap;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;

import org.age.annotation.ForTestsOnly;

import com.google.common.collect.ArrayTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Table;
import com.google.common.eventbus.EventBus;

import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

/**
 * A builder of {@link DefaultStateMachineService} instances. It offers a simple, flexible interface for creation of
 * state
 * machines.
 * <p>
 * <p>
 * Initially, a user is required to provide at least:
 * <ul>
 * <li> an enumeration of states,
 * <li> an enumeration of transitions,
 * <li> an entry state ({@link #startWith}),
 * <li> terminal states ({@link #terminateIn}),
 * <li> an event to fire on failures and errors ({@link #ifFailed}).
 * </ul>
 * Failure to do so results in {@link IllegalStateException} when {@link #build} is called.
 *
 * @param <S>
 *       the states enumeration.
 * @param <E>
 *       the events enumeration.
 *
 * @author AGH AgE Team
 */
@SuppressWarnings({ "ReturnOfThis", "ReturnOfInnerClass", "InstanceVariableMayNotBeInitialized",
        "MethodReturnOfConcreteClass" })
public final class StateMachineServiceBuilder<S extends Enum<S>, E extends Enum<E>> {

    private static final Logger log = LoggerFactory.getLogger(StateMachineServiceBuilder.class);

    private final FailureBehaviorBuilder failureBehaviorBuilder = new FailureBehaviorBuilder();

    private final Table<S, E, Set<S>> transitions;

    private final Map<E, Set<S>> noStateTransitions;

    private final Table<S, E, Consumer<FSM<S, E>>> actions;

    private final Map<E, Consumer<FSM<S, E>>> noStateActions;

    private final Class<S> stateClass;

    private final Class<E> eventClass;

    private S initialState;

    private EnumSet<S> terminalStates;

    private EventBus eventBus;

    private String name;

    private boolean shutdownWhenTerminated;

    private Class<? extends StateChangedEvent> stateChangedEventClass = StateChangedEvent.class;

    private boolean synchronous = false;

    StateMachineServiceBuilder(@NonNull final Class<S> states, @NonNull final Class<E> events) {
        stateClass = requireNonNull(states);
        eventClass = requireNonNull(events);

        transitions = ArrayTable.create(EnumSet.allOf(stateClass), EnumSet.allOf(eventClass));
        actions = ArrayTable.create(EnumSet.allOf(stateClass), EnumSet.allOf(eventClass));

        noStateTransitions = newEnumMap(eventClass);
        noStateActions = newEnumMap(eventClass);
    }

    public static <S extends Enum<S>, E extends Enum<E>> StateMachineServiceBuilder<S, E> withStatesAndEvents(
            @NonNull final Class<S> states, @NonNull final Class<E> events) {
        return new StateMachineServiceBuilder<>(states, events);
    }

    /**
     * Requests that events will be sent synchronously.
     *
     * @return this builder instance.
     */
    public StateMachineServiceBuilder<S, E> notifyWithType(
            @NonNull final Class<? extends StateChangedEvent<S, E>> klass) {
        stateChangedEventClass = requireNonNull(klass);
        return this;
    }

    public StateMachineServiceBuilder<S, E> withName(@NonNull final String name) {
        this.name = requireNonNull(name);
        return this;
    }

    /**
     * Starts the declaration of behaviour when the FSM is at the given state.
     *
     * @param state
     *       a state.
     *
     * @return an action builder.
     */
    public ActionBuilder in(final S state) {
        return new ActionBuilder(state);
    }

    /**
     * Starts the declaration of behaviour for the events that are not dependent on states.
     *
     * @return an action builder.
     */
    public AnyStateActionBuilder inAnyState() {
        return new AnyStateActionBuilder();
    }

    /**
     * Declares an initialState state.
     *
     * @param state
     *       a state.
     *
     * @return this builder instance.
     */
    public StateMachineServiceBuilder<S, E> startWith(final S state) {
        initialState = requireNonNull(state);
        log.debug("Starting state: {}.", initialState);
        return this;
    }

    /**
     * Indicates which states are terminalStates.
     *
     * @param states
     *       states that should be marked as terminalStates.
     *
     * @return this builder instance.
     */
    public StateMachineServiceBuilder<S, E> terminateIn(final S... states) {
        checkArgument(states.length > 0, "Must provide at least one terminating state.");

        terminalStates = EnumSet.copyOf(Arrays.asList(states));
        log.debug("Terminal states: {}.", terminalStates);
        return this;
    }

    /**
     * Starts the declaration of actions taken when the failure occurs.
     *
     * @return a failure behaviour builder.
     */
    public FailureBehaviorBuilder ifFailed() {
        return failureBehaviorBuilder;
    }

    public StateMachineServiceBuilder<S, E> withEventBus(final EventBus eventBus) {
        this.eventBus = eventBus;
        return this;
    }

    /**
     * Builds and returns a new service.
     *
     * @return a new {@code StateMachineService}.
     */
    public StateMachineService<S, E> build() {
        log.debug("Building a state machine: N={}, S={}, E={}.", name, stateClass, eventClass);
        checkState(nonNull(name));
        checkState(nonNull(stateClass));
        checkState(nonNull(eventClass));
        checkState(nonNull(initialState));
        checkState(nonNull(terminalStates));
        checkState(nonNull(getFailureEvent()));

        return new DefaultStateMachineService<>(this);
    }

    // Package-protected methods for service creation and testing

    @NonNull
    Class<S> stateClass() {
        assert nonNull(stateClass);
        return stateClass;
    }

    @NonNull
    Class<E> eventClass() {
        assert nonNull(eventClass);
        return eventClass;
    }

    @NonNull
    String name() {
        assert nonNull(name);
        return name;
    }

    @NonNull
    Table<S, E, Set<S>> transitions() {
        assert nonNull(transitions);
        return transitions;
    }

    @NonNull
    Table<S, E, Consumer<FSM<S, E>>> actions() {
        assert nonNull(actions);
        return actions;
    }

    @NonNull
    S initialState() {
        assert nonNull(initialState);
        return initialState;
    }

    @NonNull
    EnumSet<S> terminalStates() {
        assert nonNull(terminalStates);
        return terminalStates;
    }

    @Nullable
    EventBus eventBus() {
        return eventBus;
    }

    @Nullable
    Map<E, Set<S>> getAnyTransitions() {
        return noStateTransitions;
    }

    @Nullable
    Map<E, Consumer<FSM<S, E>>> getAnyActions() {
        return noStateActions;
    }

    @NonNull
    Method stateChangedEventCreateMethod() {
        Method eventCreateMethod;
        try {
            eventCreateMethod = stateChangedEventClass.getMethod("create", stateClass, eventClass, stateClass);
        } catch (final NoSuchMethodException ignored) {
            try {
                eventCreateMethod = stateChangedEventClass.getMethod("create", Enum.class, Enum.class, Enum.class);
            } catch (final NoSuchMethodException e1) {
                log.error("Incorrect event class.", e1);
                throw new IllegalStateException(e1);
            }
        }
        return eventCreateMethod;
    }

    @NonNull
    E getFailureEvent() {
        return failureBehaviorBuilder.event();
    }

    @NonNull
    Consumer<Throwable> getExceptionHandler() {
        return failureBehaviorBuilder.function();
    }

    @ForTestsOnly
    @NonNull
    Class<? extends StateChangedEvent> getStateChangedEventClass() {
        return stateChangedEventClass;
    }

    @ForTestsOnly
    void synchronous() {
        synchronous = true;
    }

    /**
     * Returns always false.
     * <p>
     * Unit tests should override it with true in order to process events synchronously.
     */
    @ForTestsOnly
    boolean isSynchronous() {
        return synchronous;
    }

    /**
     * Builds the transitions table.
     *
     * @return an immutable transitions table.
     */
    @NonNull
    Table<S, E, TransitionDescriptor<S, E>> buildTransitionsTable() {
        final EnumSet<S> allStates = EnumSet.allOf(stateClass);
        final EnumSet<E> allEvents = EnumSet.allOf(eventClass);
        final Table<S, E, TransitionDescriptor<S, E>> table = ArrayTable.create(allStates, allEvents);

        for (final S state : allStates) {
            for (final E event : allEvents) {
                table.put(state, event, TransitionDescriptor.nullDescriptor());
            }
        }

        for (final S state : allStates) {
            noStateTransitions.forEach((event, targetStates) -> {
                final TransitionDescriptor<S, E> descriptor = new TransitionDescriptor<>(state, event, targetStates,
                        noStateActions.get(event));
                table.put(state, event, descriptor);
            });
            transitions.row(state).forEach((event, targetStates) -> {
                if (isNull(targetStates)) {
                    return;
                }
                final TransitionDescriptor<S, E> descriptor = new TransitionDescriptor<>(state, event, targetStates,
                        actions.get(state, event));
                table.put(state, event, descriptor);
            });
        }

        if (log.isDebugEnabled()) {
            table.values().forEach(descriptor -> {
                if (nonNull(descriptor.initial())) {
                    log.debug("New transition: {}.", descriptor);
                }
            });
        }

        return ImmutableTable.copyOf(table);
    }

    /**
     * An action builder.
     *
     * @author AGH AgE Team
     */
    @SuppressWarnings("InstanceMethodNamingConvention")
    public final class ActionBuilder {

        private final S entry;

        @Nullable
        private E event;

        @Nullable
        private Set<S> exit;

        @Nullable
        private Consumer<FSM<S, E>> action;

        private ActionBuilder(@NonNull final S entry) {
            assert entry != null;
            this.entry = entry;
        }

        /**
         * Declares an event that causes the action.
         *
         * @param initiatingEvent
         *       a causing event.
         *
         * @return this action builder.
         */
        @NonNull
        public ActionBuilder on(@NonNull final E initiatingEvent) {
            requireNonNull(initiatingEvent);
            if (nonNull(event)) {
                checkState(nonNull(exit), "Declaring new event without configuring previous.");
                transitions.put(entry, event, exit);
                actions.put(entry, event, action);
                event = null;
                exit = null;
                action = null;
            }
            event = initiatingEvent;
            return this;
        }

        /**
         * Declares an action to be executed during transition.
         *
         * @param actionToExecute
         *       an action to execute.
         *
         * @return this action builder.
         */
        @NonNull
        public ActionBuilder execute(@NonNull final Consumer<FSM<S, E>> actionToExecute) {
            action = requireNonNull(actionToExecute);
            return this;
        }

        /**
         * Declares a target state.
         *
         * @param state
         *       a target state.
         *
         * @return this action builder.
         */
        @SafeVarargs
        @NonNull
        public final ActionBuilder goTo(final S... state) {
            requireNonNull(state);
            checkArgument(state.length > 0, "Empty set of targets.");

            exit = ImmutableSet.copyOf(state);
            return this;
        }

        /**
         * Finishes the action declaration.
         *
         * @return a state machine builder.
         */
        @NonNull
        public StateMachineServiceBuilder<S, E> commit() {
            checkState(nonNull(event), "Event not provided.");
            checkState(nonNull(exit), "Transition targets not provided.");
            checkState(!exit.isEmpty(), "Transition targets not provided.");

            transitions.put(entry, event, exit);
            actions.put(entry, event, action);
            return StateMachineServiceBuilder.this;
        }
    }

    /**
     * An action builder for state-independent actions.
     *
     * @author AGH AgE Team
     */
    @SuppressWarnings("InstanceMethodNamingConvention")
    public final class AnyStateActionBuilder {

        @Nullable
        private E event;

        @Nullable
        private Set<S> exit;

        @Nullable
        private Consumer<FSM<S, E>> action;

        /**
         * Declares an event that causes the action.
         *
         * @param initiatingEvent
         *       a causing event.
         *
         * @return this action builder.
         */
        @NonNull
        public AnyStateActionBuilder on(final E initiatingEvent) {
            requireNonNull(initiatingEvent);
            if (nonNull(event)) {
                checkState(nonNull(exit), "Declaring new event without configuring previous.");
                noStateTransitions.put(event, exit);
                noStateActions.put(event, action);
                event = null;
                exit = null;
                action = null;
            }
            event = initiatingEvent;
            return this;
        }

        /**
         * Declares an action to be executed during transition.
         *
         * @param actionToExecute
         *       an action to execute.
         *
         * @return this action builder.
         */
        @NonNull
        public AnyStateActionBuilder execute(final Consumer<FSM<S, E>> actionToExecute) {
            action = requireNonNull(actionToExecute);
            return this;
        }

        /**
         * Declares a target state.
         *
         * @param state
         *       a target state.
         *
         * @return this action builder.
         */
        @SafeVarargs
        @NonNull
        public final AnyStateActionBuilder goTo(final S... state) {
            requireNonNull(state);
            checkArgument(state.length > 0, "Empty set of targets.");

            exit = ImmutableSet.copyOf(state);
            return this;
        }

        /**
         * Finishes the action declaration.
         *
         * @return a state machine builder.
         */
        @NonNull
        public StateMachineServiceBuilder<S, E> commit() {
            checkState(nonNull(event), "Event not provided.");
            checkState(nonNull(exit), "Transition targets not provided.");
            checkState(!exit.isEmpty(), "Transition targets not provided.");

            noStateTransitions.put(event, exit);
            noStateActions.put(event, action);
            return StateMachineServiceBuilder.this;
        }
    }

    /**
     * A builder for internal FSM failure.
     *
     * @author AGH AgE Team
     */
    @SuppressWarnings("InstanceMethodNamingConvention")
    public final class FailureBehaviorBuilder {

        @MonotonicNonNull
        private E event;

        @MonotonicNonNull
        private Consumer<Throwable> function;

        FailureBehaviorBuilder() {
        }

        /**
         * Declares which event should be fired when failure occurs.
         *
         * @param eventToFire
         *       an event to fire.
         *
         * @return a state machine builder.
         */
        @NonNull
        public StateMachineServiceBuilder<S, E> fireAndCall(@NonNull final E eventToFire,
                @NonNull final Consumer<Throwable> exceptionHandler) {
            event = requireNonNull(eventToFire);
            function = requireNonNull(exceptionHandler);
            return StateMachineServiceBuilder.this;
        }

        @NonNull
        E event() {
            assert nonNull(event);
            return event;
        }

        @NonNull
        Consumer<Throwable> function() {
            assert nonNull(function);
            return function;
        }
    }

}