BuilderState.java

package swingtree;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import swingtree.style.ComponentExtension;
import swingtree.threading.EventProcessor;

import javax.swing.JComponent;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 *  A library internal object for modelling the state of a builder node,
 *  in particular the component wrapped by the builder node.
 *
 * @param <C> The type of the component wrapped by the builder node.
 */
final class BuilderState<C extends java.awt.Component>
{
    private static final Logger log = LoggerFactory.getLogger(BuilderState.class);

    static final String WHY_A_BUILDER_IS_DISPOSED =
                    "\nA builder is automatically disposed when it is being superseded by a\n" +
                    "new builder instance through a subsequent call to the next builder method\n" +
                    "in the chain of builder method calls.\n" +
                    "The SwingTree API only allows for writing declarative code,\n" +
                    "and the use of procedural GUI code is largely forbidden " +
                    "to ensure readability and prevent side effects.\n" +
                    "In practise, this means that you may not store and reuse references to spent builders.\n" +
                    "This is a similar design choice as in Java's Stream API,\n" +
                    "where an exception is thrown when trying to reuse a stream after it has already been consumed.\n";

    enum Mode
    {
        /**
         * The component mutations are composed into a factory pipeline and executed when the component is fetched.
         */
        FUNCTIONAL_FACTORY_BUILDER,
        /**
         *  Builder states get disposed after being used for building.
         */
        DECLARATIVE_ONLY,
        /**
         *  Builder states do not get disposed after being used for building.
         */
        PROCEDURAL_OR_DECLARATIVE
    }

    /**
     *  A builder can either be used in declarative or procedural mode.
     *  In declarative mode, builder nodes get disposed after being used for building.
     *  In procedural mode, builder nodes do not get disposed after being used for building,
     *  meaning that a builder node can be reused for component mutations.
     */
    private final Mode _mode;
    /**
     * The event processor determines the thread execution mode of the component and view model events.
     * And also which type of thread can access the component.
     */
    private final EventProcessor _eventProcessor;
    /**
     *  The type class of the component managed by this builder.
     */
    private final Class<C> _componentType;

    /**
     *  A supplier for the component managed by this builder.
     *  The supplier is null when the builder is disposed.
     */
    private @Nullable Supplier<C> _componentFetcher; // Is null when the builder is disposed.


    <T extends C> BuilderState( Class<T> type, Supplier<C> componentSource )
    {
        this(
            SwingTree.get().getEventProcessor(),
            Mode.FUNCTIONAL_FACTORY_BUILDER,
            (Class<C>) type,
            ()->initializeComponent(componentSource.get()).get()
        );
    }

    BuilderState( C component )
    {
        this(
            SwingTree.get().getEventProcessor(),
            Mode.DECLARATIVE_ONLY,
            (Class<C>) component.getClass(),
            initializeComponent(component)
        );
    }

    BuilderState(
        EventProcessor        eventProcessor,
        Mode                  mode,
        Class<C>              type,
        @Nullable Supplier<C> componentFetcher
    ) {
        Objects.requireNonNull(eventProcessor,   "eventProcessor");
        Objects.requireNonNull(mode,             "mode");
        Objects.requireNonNull(type,             "type");
        Objects.requireNonNull(componentFetcher, "componentFetcher");

        _eventProcessor   = eventProcessor;
        _mode             = mode;
        _componentType    = type;
        _componentFetcher = componentFetcher;
    }

    private static <C extends java.awt.Component> Supplier<C> initializeComponent( C component )
    {
        Objects.requireNonNull(component, "component");
        if ( component instanceof JComponent)
            ComponentExtension.initializeFor( (JComponent) component );

        return () -> component;
    }

    /**
     *  @return The component managed by this builder.
     *  @throws IllegalStateException If this builder state is disposed (it's reference to the component is null).
     */
    C component()
    {
        if ( this.isDisposed() )
            throw new IllegalStateException(
                    "Trying to access the component of a spent and disposed builder!" +
                    WHY_A_BUILDER_IS_DISPOSED +
                    "If you need to access the component of a builder node, " +
                    "you may only do so through the builder instance returned by the most recent builder method call."
                );
        if ( _componentFetcher == null )
            throw new IllegalStateException("This builder state is disposed and cannot be used for building.");

        return _componentType.cast(_componentFetcher.get());
    }

    /**
     * The thread mode determines how events are dispatched to the component.
     * And also which type of thread can access the component. <br>
     * <b>This will never return null.</b>
     */
    EventProcessor eventProcessor() {
        return _eventProcessor;
    }

    /**
     *  The type class of the component managed by this builder. <br>
     *  <b>This will never return null.</b>
     */
    Class<C> componentType() {
        return _componentType;
    }

    /**
     *  Cut off the strong reference to the component supplier managed by this builder
     *  and disposes this builder node as a whole, meaning it is no longer usable for building. <br>
     *  <b>Only call this method from the UI thread (AWT's EDT thread) as builder states are not thread safe.</b>
     */
    void dispose() {
        if ( !UI.thisIsUIThread() ) {
            Thread currentThread = Thread.currentThread();
            if ( !currentThread.getName().startsWith("Test worker") )
                log.warn(
                    "The builder state for component type '" + _componentType.getSimpleName() + "' " +
                    "is being disposed from thread '" + currentThread.getName() + "', which is problematic! \n" +
                    "Builder states should only be disposed by the UI thread (AWT's EDT thread) because " +
                    "they lack thread safety. Furthermore, it is important to note that GUI components " +
                    "should only be assembled in the frontend layer of the application, and not in the backend layer " +
                    "and one of its threads.",
                    new Throwable()
                );
        }
        _componentFetcher = null;
    }

    /**
     *  A builder may be disposed, which means that it is no longer usable for building
     *  as it no longer has a reference to the component or built steps. <br>
     *  @return True if this builder node has already been disposed.
     */
    boolean isDisposed() {
        return _componentFetcher == null;
    }

    /**
     *  A mutator is a functional consumer containing an action
     *  that mutates the component passed to it. <br>
     *  A mutator is either executed immediately or composed into a factory pipeline
     *  and executed when the component is built and fetched at the end of the builder chain. <br>
     *
     * @param componentMutator A consumer which mutates the component managed by this builder.
     * @return In procedural mode, this very builder node is returned.
     *         In declarative mode, a new builder node is returned which is a copy of this builder node,
     *         and this builder node is disposed.
     *         Either way, the component managed by this builder is mutated by the provided consumer.
     */
    BuilderState<C> withMutator( Consumer<C> componentMutator )
    {
        if ( this.isDisposed() )
            throw new IllegalStateException(
                    "Trying to build using a builder which has already been spent and disposed!" +
                    WHY_A_BUILDER_IS_DISPOSED +
                    "Make sure to only use the builder instance returned by the most recent builder method call."
                );

        if ( _mode != Mode.FUNCTIONAL_FACTORY_BUILDER)
            try {
                if ( _componentFetcher != null )
                    componentMutator.accept(_componentFetcher.get());
            } catch ( Exception e ) {
                e.printStackTrace();
                log.error(
                    "Exception while building component of type '" + _componentType.getSimpleName() + "'.", e
                );
                /*
                    If individual steps in the builder chain throw exceptions,
                    we do not want the entire GUI declaration to fail
                    so that only the GUI of the failing component is not built.
                */
            }

        switch ( _mode) 
        {
            case FUNCTIONAL_FACTORY_BUILDER:
            {
                Supplier<C> componentFactory = _componentFetcher;
                this.dispose(); // detach strong reference to the component to allow it to be garbage collected.
                return new BuilderState<>(
                        _eventProcessor,
                        _mode,
                        _componentType,
                        () -> {
                            if ( componentFactory == null )
                                throw new IllegalStateException("This builder state is disposed and cannot be used for building.");
                            C newComponent = componentFactory.get();
                            componentMutator.accept(newComponent);
                            return newComponent;
                        }
                );
            }
            case DECLARATIVE_ONLY:
            {
                @Nullable Supplier<C> componentFactory = _componentFetcher;
                this.dispose(); // detach strong reference to the component to allow it to be garbage collected.
                return new BuilderState<>(
                        _eventProcessor,
                        _mode,
                        _componentType,
                        componentFactory
                    );
            }
            case PROCEDURAL_OR_DECLARATIVE:
            {
                return this;
            }
            default:
                throw new IllegalStateException("Unknown mode: " + _mode);
        }
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "[" + _componentType.getSimpleName() + "]";
    }

    @Override
    public boolean equals( Object o ) {
        if ( this == o ) return true;
        if ( o == null || getClass() != o.getClass() ) return false;

        BuilderState<?> that = (BuilderState<?>) o;

        return _componentType.equals(that._componentType) && _mode == that._mode;
    }

    @Override
    public int hashCode() {
        return _componentType.hashCode();
    }
}