All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.collaborationengine.ComponentConnectionContext Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 Vaadin Ltd
 *
 * This program is available under Commercial Vaadin Runtime License 1.0
 * (CVRLv1).
 *
 * For the full License, see http://vaadin.com/license/cvrl-1
 */
package com.vaadin.collaborationengine;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.internal.DeadlockDetectingCompletableFuture;
import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.server.Command;
import com.vaadin.flow.server.Version;
import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.shared.communication.PushMode;

/**
 * A connection context based on the attach state of a set of component
 * instances. The context is considered active whenever at least one tracked
 * component is attached. All attached components must belong to the same UI
 * instance, and this UI instance is used to dispatch actions using
 * {@link UI#access(Command)}.
 *
 * @author Vaadin Ltd
 * @since 1.0
 */
public class ComponentConnectionContext implements ConnectionContext {

    private enum State {
        /**
         * Fully active. ui has been set.
         */
        ACTIVE,
        /**
         * Inactivation has been initiated. ui is still set, but will be cleared
         * as soon as the inbox is purged.
         */
        INACTIVATING,
        /**
         * Fully inactive. ui has been cleared.
         */
        INACTIVE;
    }

    private final Map componentListeners = new HashMap<>();
    private final Set attachedComponents = new HashSet<>();

    private volatile UI ui;

    private final ExecutionQueue inbox = new ExecutionQueue();
    private final ExecutionQueue shutdownCommands = new ExecutionQueue();
    private final ActionDispatcher actionDispatcher = new ActionDispatcherImpl();
    private final AtomicReference state = new AtomicReference<>(
            State.INACTIVE);
    private Consumer activationHandler;
    private Executor backgroundRunner;
    private Registration beaconListener;
    private Registration destroyListener;

    private static AtomicBoolean pushWarningShown = new AtomicBoolean(false);

    /**
     * Creates an empty component connection context.
     *
     * @since 1.0
     */
    public ComponentConnectionContext() {
        // Nothing to do here
    }

    /**
     * Creates a new component connection context which is initially using a
     * single component.
     *
     * @param component
     *            the component to use, not null
     *
     * @since 1.0
     */
    public ComponentConnectionContext(Component component) {
        addComponent(component);
    }

    /**
     * Adds a component instance to track for this context. Calling this method
     * again with a component that is already tracked has no effect.
     *
     * @param component
     *            the component to track, not null
     * @see #removeComponent(Component)
     *
     * @since 1.0
     */
    public void addComponent(Component component) {
        Objects.requireNonNull(component, "Component can't be null.");

        if (!componentListeners.containsKey(component)) {
            Registration attachRegistration = component.addAttachListener(
                    event -> markAsAttached(event.getUI(), event.getSource()));
            Registration detachRegistration = component.addDetachListener(
                    event -> markAsDetached(event.getSource()));

            componentListeners.put(component, Registration
                    .combine(attachRegistration, detachRegistration));

            component.getUI().ifPresent(
                    componentUi -> markAsAttached(componentUi, component));
        }
    }

    /**
     * Stops tracking a component for this context. Calling this method for a
     * component that isn't tracked has no effect.
     *
     * @param component
     *            the component to stop tracking, not null
     * @see #addComponent(Component)
     *
     * @since 1.0
     */
    public void removeComponent(Component component) {
        Objects.requireNonNull(component, "Component can't be null.");

        Registration registration = componentListeners.remove(component);
        if (registration != null) {
            registration.remove();
            markAsDetached(component);
        }
    }

    private void markAsAttached(UI componentUi, Component component) {
        if (attachedComponents.add(component)) {
            if (attachedComponents.size() == 1) {
                // First attach
                this.ui = componentUi;

                checkForPush(ui);

                BeaconHandler beaconHandler = BeaconHandler
                        .ensureInstalled(this.ui);
                beaconListener = beaconHandler
                        .addListener(this::deactivateConnection);

                ServiceDestroyDelegate destroyDelegate = ServiceDestroyDelegate
                        .ensureInstalled(this.ui);
                destroyListener = destroyDelegate
                        .addListener(event -> deactivateConnection());

                flushPendingActionsIfActive();

                if (activationHandler != null) {
                    activate();
                }

            } else if (componentUi != ui) {
                throw new IllegalStateException(
                        "All components in this connection context must be associated with the same UI.");
            }
        }
    }

    private void markAsDetached(Component component) {
        if (attachedComponents.remove(component)) {
            if (attachedComponents.isEmpty()) {
                // Last detach
                deactivateConnection();
            }
        }
    }

    @Override
    public Registration init(ActivationHandler activationHandler,
            Executor backgroundRunner) {
        if (this.activationHandler != null) {
            throw new IllegalStateException(
                    "This context has already been initialized");
        }
        this.activationHandler = Objects.requireNonNull(activationHandler,
                "Activation handler cannot be null");
        this.backgroundRunner = Objects.requireNonNull(backgroundRunner,
                "Background runner cannot be null");

        if (this.ui != null) {
            activate();
        }
        CompletableFuture deactivationFuture = new CompletableFuture<>();
        return new AsyncRegistration(deactivationFuture, () -> {
            // This instance won't be used again, release all references
            if (state.get() == State.INACTIVE) {
                // If already inactive, complete now
                deactivationFuture.complete(null);
            } else {
                // Otherwise, complete when it becomes inactive
                shutdownCommands.add(() -> deactivationFuture.complete(null));
            }

            componentListeners.values().forEach(Registration::remove);
            componentListeners.clear();
            attachedComponents.clear();
            deactivateConnection();
        });
    }

    private void activate() {
        if (this.activationHandler != null
                && state.getAndSet(State.ACTIVE) != State.ACTIVE) {
            this.activationHandler.accept(this.actionDispatcher);
        }
    }

    private void deactivateConnection() {
        if (beaconListener != null) {
            beaconListener.remove();
            beaconListener = null;
        }
        if (destroyListener != null) {
            destroyListener.remove();
            destroyListener = null;
        }
        if (activationHandler != null && ui != null
                && state.compareAndSet(State.ACTIVE, State.INACTIVATING)) {
            activationHandler.accept(null);
            if (inbox.isEmpty()) {
                inactivateIfDeactivating();
            }
        }
    }

    private void inactivateIfDeactivating() {
        if (state.compareAndSet(State.INACTIVATING, State.INACTIVE)) {
            this.ui = null;
            shutdownCommands.runPendingCommands();
        }
    }

    class ActionDispatcherImpl implements ActionDispatcher {
        /**
         * Executes the given action by holding the session lock. This is done
         * by using {@link UI#access(Command)} on the UI that the component(s)
         * associated with this context belong to. This ensures that any UI
         * changes are pushed to the client in real-time if {@link Push} is
         * enabled.
         * 

* If this context is not active (none of the components are attached to * a UI), the action is postponed until the connection becomes active. * * @param action * the action to dispatch */ @Override public void dispatchAction(Command action) { inbox.add(action); flushPendingActionsIfActive(); } @Override public CompletableFuture createCompletableFuture() { UI localUI = ComponentConnectionContext.this.ui; if (localUI == null) { throw new IllegalStateException( "The topic connection within this context maybe deactivated." + "Make sure the context has at least one component attached to the UI."); } return new DeadlockDetectingCompletableFuture<>( localUI.getSession()); } } private void flushPendingActionsIfActive() { UI localUI = this.ui; if (localUI == null || backgroundRunner == null) { return; } backgroundRunner.execute(() -> localUI.access(() -> { inbox.runPendingCommands(); inactivateIfDeactivating(); })); } private static void checkForPush(UI ui) { if (!canPushChanges(ui) && isActivationEnabled(ui)) { ui.getPushConfiguration().setPushMode(PushMode.AUTOMATIC); boolean warningAlreadyShown = pushWarningShown.getAndSet(true); if (!warningAlreadyShown) { int flowVersionInVaadin14 = 2; String annotationLocation = Version .getMajorVersion() == flowVersionInVaadin14 ? "root layout or individual views" : "AppShellConfigurator class"; LoggerFactory.getLogger(ComponentConnectionContext.class).warn( "Server push has been automatically enabled so updates can be shown immediately. " + "Add @Push annotation on your " + annotationLocation + " to suppress this warning. " + "Set automaticallyActivatePush to false in CollaborationEngineConfiguration if you want to ensure push is not automatically enabled."); } } } private static boolean isActivationEnabled(UI ui) { CollaborationEngine ce = CollaborationEngine .getInstance(ui.getSession().getService()); return ce != null ? ce.getConfiguration().isAutomaticallyActivatePush() : CollaborationEngineConfiguration.DEFAULT_AUTOMATICALLY_ACTIVATE_PUSH; } private static boolean canPushChanges(UI ui) { return ui.getPushConfiguration().getPushMode().isEnabled() || ui.getPollInterval() > 0; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy