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

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

/*
 * Copyright 2020-2022 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.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.vaadin.collaborationengine.CollaborationMessagePersister.FetchQuery;
import com.vaadin.collaborationengine.CollaborationMessagePersister.PersistRequest;
import com.vaadin.collaborationengine.MessageHandler.MessageContext;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.shared.Registration;

/**
 * Manager to handle messages sent to a topic. It allows submitting messages to
 * a topic and set a handler to react when a new message has been submitted.
 *
 * @author Vaadin Ltd
 */
public class MessageManager extends AbstractCollaborationManager {

    static {
        UsageStatistics.markAsUsed(
                CollaborationEngine.COLLABORATION_ENGINE_NAME
                        + "/MessageManager",
                CollaborationEngine.COLLABORATION_ENGINE_VERSION);
    }

    private static final Object FETCH_LOCK = new Object();

    private static final String MISSING_RECENT_MESSAGES = "The messages "
            + "returned invoking CollaborationMessagePersister.fetchMessages() "
            + "do not include the last fetched message of the previous call. "
            + "Please update the implementation to fetch all messages whose "
            + "timestamp is greater OR EQUAL with the query's timestamp.";

    static final String LIST_NAME = MessageManager.class.getName();

    private final CollaborationMessagePersister persister;

    private CollaborationList list;

    private MessageHandler messageHandler;

    private CollaborationMessage lastSeenMessage;

    private ListKey lastMessageKey;

    private boolean catchupMode = false;

    private final Map, CollaborationMessage> pendingMessageFutures = new LinkedHashMap<>();

    private final Map> persistedMessageFutures = new LinkedHashMap<>();

    /**
     * Creates a new manager for the given component.
     *
     * @param component
     *            the component which holds UI access, not {@code null}
     * @param localUser
     *            the information of the local user, not {@code null}
     * @param topicId
     *            the id of the topic to connect to, not {@code null}
     */
    public MessageManager(Component component, UserInfo localUser,
            String topicId) {
        this(component, localUser, topicId, null);
    }

    /**
     * Creates a new persisting manager for the given component.
     *
     * @param component
     *            the component which holds UI access, not {@code null}
     * @param localUser
     *            the information of the local user, not {@code null}
     * @param topicId
     *            the id of the topic to connect to, not {@code null}
     * @param persister
     *            the persister to read/write messages to an external source
     */
    public MessageManager(Component component, UserInfo localUser,
            String topicId, CollaborationMessagePersister persister) {
        this(new ComponentConnectionContext(component), localUser, topicId,
                persister, CollaborationEngine.getInstance());
    }

    /**
     * Creates a new manager for the given connection context.
     *
     * @param context
     *            the context that manages connection status, not {@code null}
     * @param localUser
     *            the information of the local user, not {@code null}
     * @param topicId
     *            the id of the topic to connect to, not {@code null}
     * @param collaborationEngine
     *            the collaboration engine instance to use, not {@code null}
     */
    public MessageManager(ConnectionContext context, UserInfo localUser,
            String topicId, CollaborationEngine collaborationEngine) {
        this(context, localUser, topicId, null, collaborationEngine);
    }

    /**
     * Creates a new persisting manager for the given connection context.
     *
     * @param context
     *            the context that manages connection status, not {@code null}
     * @param localUser
     *            the information of the local user, not {@code null}
     * @param topicId
     *            the id of the topic to connect to, not {@code null}
     * @param persister
     *            the persister to read/write messages to an external source
     * @param collaborationEngine
     *            the collaboration engine instance to use, not {@code null}
     */
    public MessageManager(ConnectionContext context, UserInfo localUser,
            String topicId, CollaborationMessagePersister persister,
            CollaborationEngine collaborationEngine) {
        super(localUser, topicId, collaborationEngine);
        this.persister = persister;
        openTopicConnection(context, this::onConnectionActivate);
    }

    /**
     * Sets a handler which will be invoked for all messages already in the
     * topic and when a new message is submitted.
     * 

* The handler accepts a {@link MessageContext} as a parameter which * contains a reference to the sent message. * * @param handler * the message handler, or {@code null} to remove an existing * handler */ public void setMessageHandler(MessageHandler handler) { messageHandler = handler; lastSeenMessage = null; catchupMode = false; if (messageHandler != null) { getMessages().forEach(this::applyHandler); } } /** * Submits a message to the topic as the current local user. * * @param text * the text of the message, not {@code null} * @return a future which will complete when the message has been added to * the topic, not {@code null} */ public CompletableFuture submit(String text) { Objects.requireNonNull(text); UserInfo user = getLocalUser(); Instant now = getCollaborationEngine().getClock().instant(); CollaborationMessage message = new CollaborationMessage(user, text, now); return submit(message); } /** * Submits a message to the topic. * * @param message * the message, not {@code null} * @return a future which will complete when the message has been added to * the topic, not {@code null} */ public CompletableFuture submit(CollaborationMessage message) { Objects.requireNonNull(message); if (list == null) { CompletableFuture future = new CompletableFuture<>(); pendingMessageFutures.put(future, message); return future; } else { return appendOrPersist(message); } } private CompletableFuture appendOrPersist( CollaborationMessage message) { if (persister != null) { String topicId = getTopicId(); PersistRequest request = new PersistRequest(this, topicId, message); persister.persistMessage(request); CompletableFuture future = new CompletableFuture<>(); persistedMessageFutures.put(message, future); fetchPersistedList(); return future; } else { return list.insertLast(message).getCompletableFuture(); } } private Registration onConnectionActivate(TopicConnection topicConnection) { list = topicConnection.getNamedList(LIST_NAME); list.subscribe(this::onListChange); fetchPersistedList(); pendingMessageFutures.entrySet().removeIf(entry -> { CompletableFuture future = entry.getKey(); CollaborationMessage message = entry.getValue(); appendOrPersist(message).whenComplete((result, throwable) -> { if (throwable != null) { future.completeExceptionally(throwable); } else { future.complete(result); } }); return true; }); return this::onConnectionDeactivate; } private void onConnectionDeactivate() { list = null; catchupMode = true; } private void onListChange(ListChangeEvent event) { CollaborationMessage message = event .getValue(CollaborationMessage.class); lastMessageKey = event.getKey(); if (message != null) { CompletableFuture future = persistedMessageFutures .remove(message); if (future != null) { future.complete(null); } applyHandler(message); } } private void applyHandler(CollaborationMessage message) { if (!catchupMode) { lastSeenMessage = message; if (messageHandler != null) { MessageContext context = new DefaultMessageContext(message); messageHandler.handleMessage(context); } } else if (message.equals(lastSeenMessage)) { catchupMode = false; } } private void fetchPersistedList() { if (persister != null && list != null) { String topicId = getTopicId(); synchronized (FETCH_LOCK) { List recentMessages = getRecentMessages(); Instant since = recentMessages.isEmpty() ? Instant.EPOCH : recentMessages.get(0).getTime(); FetchQuery query = new FetchQuery(this, topicId, since); List messages = persister .fetchMessages(query) .sorted(Comparator .comparing(CollaborationMessage::getTime)) .filter(message -> !recentMessages.remove(message)) .collect(Collectors.toList()); if (!recentMessages.isEmpty()) { throw new IllegalStateException(MISSING_RECENT_MESSAGES); } if (!messages.isEmpty()) { query.throwIfPropsNotUsed(); insertPersistedMessages(messages); } } } } private void insertPersistedMessages(List messages) { ListKey ifLast = lastMessageKey; List> futures = new ArrayList<>(); for (CollaborationMessage message : messages) { ListOperation op = ListOperation.insertLast(message); if (ifLast != null) { op.ifLast(ifLast); } else { op.ifEmpty(); } ListOperationResult insert = list.apply(op); futures.add(insert.getCompletableFuture()); ifLast = insert.getKey(); } CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) .thenAccept(result -> fetchPersistedList()); } private List getRecentMessages() { List messages = getMessages() .collect(Collectors.toList()); CollaborationMessage lastMessage = messages.isEmpty() ? null : messages.get(messages.size() - 1); List recentMessages = new ArrayList<>(); if (lastMessage != null) { Instant lastMessageTime = lastMessage.getTime(); for (int i = messages.size() - 1; i >= 0; i--) { CollaborationMessage m = messages.get(i); if (m.getTime().equals(lastMessageTime)) { recentMessages.add(m); } else { break; } } } return recentMessages; } // Package protected for testing Stream getMessages() { if (list != null) { return list.getItems(CollaborationMessage.class).stream(); } else { return Stream.empty(); } } static class DefaultMessageContext implements MessageContext { private final CollaborationMessage message; public DefaultMessageContext(CollaborationMessage message) { this.message = message; } @Override public CollaborationMessage getMessage() { return message; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy