Send and Receive Messages Programmatically

Collaboration Engine includes a message manager to handle text messages in a topic. You can use it, for example, for subscribing to incoming messages and submitting new messages programmatically. The manager can also be configured to persist messages to a custom backend, such as a database, so that the messages are restored when the application restarts. It provides a flexible way to manage messages in a topic and helps create custom components with collaborative messaging features.

Handling Messages Sent to a Topic

To handle messages sent to a topic, you need to create a MessageManager instance for the topic and then set a MessageHandler with setMessageHandler(). Once set, the handler is first invoked for all the messages already in the topic and later for each new message submitted to the topic. The handler can be expressed as a lambda that gets a MessageContext instance, which provides access to the CollaborationMessage containing all the details of the submitted message.

The following example shows how to create a new MessageManager instance and receive messages for a given topic:

UserInfo localUser = new UserInfo("john");
String topicId = "notifications";

MessageManager messageManager =
        new MessageManager(this, localUser, topicId); 1

messageManager.setMessageHandler(context -> { 2
    CollaborationMessage message = context.getMessage();
    UserInfo user = message.getUser();
    String text = message.getText();

    Notification.show(user.getName() + ": " + text);
});
  1. The manager is constructed with a component instance, the local user-info, and the topic ID.

  2. The setMessageHandler() method takes a callback that will be invoked when a new message is submitted. The message is provided by the context passed as the argument to the callback.

Submitting New Messages to a Topic

You can submit a new message to a topic by using submit() in MessageManager. It has two overloads: one that takes the message text as a string and submits it to the associated topic with the local user as the author of the message and with the current timestamp; the other takes a CollaborationMessage instance where you can explicitly set the message details.

The following example uses the MessageManager instance created earlier to broadcast notifications to all users connected to the same view:

add(new MessageInput(event -> {
    String text = event.getValue();
    messageManager.submit(text);
}));

The above example uses the MessageInput component to input messages, and handles them in a submit listener.

Note
Compatibility with CollaborationMessageList
If a MessageManager instance is using the same topic ID of a CollaborationMessageList, messages submitted with the manager will also appear on the list.

Persisting Messages

The data in Collaboration Engine is by default stored only in the application memory. This applies also to messages handled by MessageManager, which means that all the messages will be lost when the server is restarted.

To make the messages persistent, you need to configure MessageManager to store the messages in your own backend. It requires implementing the CollaborationMessagePersister interface.

The interface specifies two operations:

  • Store a new message.

  • For a given topic, fetch all messages that have been submitted during or after a given timestamp.

In the following example implementation, you can see how the persister is mapping between the CollaborationMessage objects and the application’s own Message entities, as well as integrating with the service layer:

@SpringComponent
public class MyMessagePersister implements CollaborationMessagePersister {

    private final MessageService messageService; 1
    private final UserService userService; 1

    public MyMessagePersister(MessageService messageService,
            UserService userService) {
        this.messageService = messageService;
        this.userService = userService;
    }

    @Override
    public Stream<CollaborationMessage> fetchMessages(FetchQuery query) {
        return messageService
                .findAllByTopicSince(query.getTopicId(), query.getSince())
                .map(messageEntity -> {
                    User author = messageEntity.getAuthor(); 1
                    UserInfo userInfo = new UserInfo(author.getId(),
                            author.getName(), author.getImageUrl());

                    return new CollaborationMessage(userInfo,
                            messageEntity.getText(), messageEntity.getTime());
                });
    }

    @Override
    public void persistMessage(PersistRequest request) {
        CollaborationMessage message = request.getMessage();

        Message messageEntity = new Message(); 1
        messageEntity.setTopic(request.getTopicId());
        messageEntity.setText(message.getText());
        messageEntity.setAuthor(userService.findById(
            message.getUser().getId()));

        // Set the time from the message only as a fallback option if your
        // database can't automatically add an insertion timestamp:
        // messageEntity.setTime(message.getTime());

        messageService.save(messageEntity);
    }
}
  1. In the example, MessageService, UserService, Message, and User are application-specific example classes, and not part of the Collaboration Engine API.

Tip
Initialize a persister from lambdas
You can also create a message persister instance with lambdas by using the static CollaborationMessagePersister.fromCallbacks() method.

It is best to configure your database to insert the creation/insertion timestamp of the entity. This ensures that the messages are ordered by their timestamps in the database. If you use the timestamp provided by CollaborationMessage::getTime() (based on the JVM clock), you take a risk of skipping some inserted messages in rare edge cases.

For example, if you use Hibernate to create the database table, you can include an automatic insertion timestamp in your entity class with the following annotation:

@Column(columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private Instant time;

You can set the message persister in the manager’s constructor:

MessageManager createManagerWithPersister(MyMessagePersister persister) {
    UserInfo localUser = new UserInfo("john");
    String topicId = "notifications";
    return new MessageManager(this, localUser, topicId, persister);
}

When a persister is provided, MessageManager takes care of keeping the component state in sync with your backend. When the first manager connects to a topic after starting the application, it fetches all the messages from the persister (making a FetchQuery with the Unix epoch as the getSince() timestamp). The fetched messages are stored in Collaboration Engine’s memory and shared among other managers connected to the same topic, avoiding redundant backend queries.

When a new message is submitted, the data flows through your backend. First, the manager saves the message to your backend with the persister. Then, the manager makes a query to the persister to fetch all the messages with a timestamp that is equal or more recent than the last message in memory. The new messages are again stored in the Collaboration Engine’s memory, updating all the manager instances that are connected to the same topic.

When implementing the fetchMessages() method, it is important to note that the returned stream should include the messages sent exactly at the time that you can get with getSince() from the FetchQuery. This ensures that all messages are fetched even when multiple messages have the same timestamp. The component takes care of filtering out the duplicates, which are expected when returning the latest message again.