WebsocketMessagePreparer.java
/*
* The coLAB project
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
*
* Licensed under the MIT License
*/
package ch.colabproject.colab.api.ws;
import ch.colabproject.colab.api.model.WithWebsocketChannels;
import ch.colabproject.colab.api.persistence.jpa.card.CardTypeDao;
import ch.colabproject.colab.api.persistence.jpa.project.ProjectDao;
import ch.colabproject.colab.api.persistence.jpa.team.TeamMemberDao;
import ch.colabproject.colab.api.persistence.jpa.user.UserDao;
import ch.colabproject.colab.api.ws.channel.model.WebsocketChannel;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ChannelsBuilder;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ForAdminChannelsBuilder;
import ch.colabproject.colab.api.ws.message.IndexEntry;
import ch.colabproject.colab.api.ws.message.PrecomputedWsMessages;
import ch.colabproject.colab.api.ws.message.WsMessage;
import ch.colabproject.colab.api.ws.message.WsUpdateMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.EncodeException;
import java.util.*;
/**
* Some convenient methods to help sending data through websockets.
*
* @author maxence
*/
public class WebsocketMessagePreparer {
/** logger */
private static final Logger logger = LoggerFactory.getLogger(WebsocketMessagePreparer.class);
/**
* never-called private constructor
*/
private WebsocketMessagePreparer() {
throw new UnsupportedOperationException(
"This is a utility class and cannot be instantiated");
}
/**
* Get the message that will be sent through the given channel.
*
* @param messagesByChannel all messagesByChannels
* @param key key to select to correct message
*
* @return the message
*/
private static WsUpdateMessage getOrCreateWsUpdateMessage(
Map<WebsocketChannel, List<WsMessage>> messagesByChannel,
WebsocketChannel key
) {
logger.trace("GetOrCreate WsUpdateMessge for channel {}", key);
if (!messagesByChannel.containsKey(key)) {
logger.trace(" -> create channel {}", key);
messagesByChannel.put(key, List.of(new WsUpdateMessage()));
return (WsUpdateMessage) messagesByChannel.get(key).get(0);
} else {
List<WsMessage> get = messagesByChannel.get(key);
logger.trace(" -> use existing channel {} := {}", key, get);
Optional<WsMessage> find = get.stream()
.filter(message -> message instanceof WsUpdateMessage)
.findFirst();
if (find.isPresent()) {
logger.trace(" -> use existing message {}", find.get());
return (WsUpdateMessage) find.get();
} else {
logger.trace(" -> create emtpy message");
WsUpdateMessage wsUpdateMessage = new WsUpdateMessage();
get.add(wsUpdateMessage);
return wsUpdateMessage;
}
}
}
/**
* Add entity to the set identified by the channel.
*
* @param messagesByChannel all sets
* @param channel channel to identify correct set
* @param entity entity to add
*/
private static void addAsUpdated(
Map<WebsocketChannel, List<WsMessage>> messagesByChannel,
WebsocketChannel channel,
WithWebsocketChannels entity) {
Collection<WithWebsocketChannels> set = WebsocketMessagePreparer
.getOrCreateWsUpdateMessage(messagesByChannel, channel).getUpdated();
logger.trace("Add {} to updated set {}", entity, set);
set.add(entity);
if (logger.isTraceEnabled()) {
set.forEach(e -> {
logger.trace("Entity: {}", e);
logger.trace("Entity hashCode: {}", e.hashCode());
logger.trace("Entity equals new: {}", e.equals(entity));
});
}
}
/**
* Add index entry to the set identified by the channel.
*
* @param byChannels all sets
* @param channel channel to identify correct set
* @param entry entry to add
*/
private static void addAsDeleted(Map<WebsocketChannel, List<WsMessage>> byChannels,
WebsocketChannel channel,
IndexEntry entry) {
Collection<IndexEntry> set = WebsocketMessagePreparer
.getOrCreateWsUpdateMessage(byChannels, channel)
.getDeleted();
logger.trace("Add {} to deleted set {}", entry, set);
set.add(entry);
}
/**
* Prepare all WsUpdateMessage.
*
* @param userDao provide userDao to resolve nested channels
* @param teamDao provide teamDao to resolve nested channels
* @param cardTypeDao provide cardTypeDao to resolve nested channels
* @param projectDao provide projectDao to resolve nested channels
* @param updated set of created/updated entities
* @param deleted set of just destroyed-entities index entry
*
* @return the precomputed messagesByChannels
*
* @throws EncodeException if creating JSON messagesByChannels failed
*/
public static PrecomputedWsMessages prepareWsMessage(
UserDao userDao,
TeamMemberDao teamDao,
CardTypeDao cardTypeDao,
ProjectDao projectDao,
Set<WithWebsocketChannels> updated,
Set<IndexEntry> deleted
) throws EncodeException {
Map<WebsocketChannel, List<WsMessage>> messagesByChannel = new HashMap<>();
logger.debug("Prepare WsMessage. Update:{}; Deleted:{}", updated, deleted);
updated.forEach(object -> {
logger.trace("Process updated entity {}", object);
object.getChannelsBuilder().computeChannels(userDao, teamDao, cardTypeDao, projectDao)
.forEach(channel -> {
addAsUpdated(messagesByChannel, channel, object);
});
});
deleted.forEach(object -> {
logger.trace("Process deleted entry {}", object);
object.getChannelsBuilder().computeChannels(userDao, teamDao, cardTypeDao, projectDao)
.forEach(
channel -> {
addAsDeleted(messagesByChannel, channel, object);
});
});
return PrecomputedWsMessages.build(messagesByChannel);
}
/**
* Prepare one message on admin channel
*
* @param userDao provide userDao to resolve nested channels
* @param message the message
*
* @return the precomputedMessage
*
* @throws EncodeException if json-encoding failed
*/
public static PrecomputedWsMessages prepareWsMessageForAdmins(
UserDao userDao,
WsMessage message
) throws EncodeException {
return prepareWsMessage(userDao, null, null, null, new ForAdminChannelsBuilder(), message);
}
/**
* Prepare one message for many channels
*
* @param userDao provide userDao to resolve nested channels
* @param teamDao provide teamDao to resolve nested channels
* @param cardTypeDao provide cardTypeDao to resolve nested channels
* @param projectDao provide projectDao to resolve nested channels
* @param channelBuilder the channel builder that defines which channels must be used
* @param message the message
* @return the precomputedMessage
* @throws EncodeException if json-encoding failed
*/
public static PrecomputedWsMessages prepareWsMessage(
UserDao userDao,
TeamMemberDao teamDao,
CardTypeDao cardTypeDao,
ProjectDao projectDao,
ChannelsBuilder channelBuilder,
WsMessage message
) throws EncodeException {
Map<WebsocketChannel, List<WsMessage>> messagesByChannel = new HashMap<>();
channelBuilder.computeChannels(userDao, teamDao, cardTypeDao, projectDao).forEach(channel -> {
messagesByChannel.put(channel, List.of(message));
});
return PrecomputedWsMessages.build(messagesByChannel);
}
}