View Javadoc
1   /*
2    * The coLAB project
3    * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4    *
5    * Licensed under the MIT License
6    */
7   package ch.colabproject.colab.api.ws;
8   
9   import ch.colabproject.colab.api.model.WithWebsocketChannels;
10  import ch.colabproject.colab.api.persistence.jpa.card.CardTypeDao;
11  import ch.colabproject.colab.api.persistence.jpa.project.ProjectDao;
12  import ch.colabproject.colab.api.persistence.jpa.team.TeamMemberDao;
13  import ch.colabproject.colab.api.persistence.jpa.user.UserDao;
14  import ch.colabproject.colab.api.ws.channel.model.WebsocketChannel;
15  import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ChannelsBuilder;
16  import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ForAdminChannelsBuilder;
17  import ch.colabproject.colab.api.ws.message.IndexEntry;
18  import ch.colabproject.colab.api.ws.message.PrecomputedWsMessages;
19  import ch.colabproject.colab.api.ws.message.WsMessage;
20  import ch.colabproject.colab.api.ws.message.WsUpdateMessage;
21  import org.slf4j.Logger;
22  import org.slf4j.LoggerFactory;
23  
24  import javax.websocket.EncodeException;
25  import java.util.*;
26  
27  /**
28   * Some convenient methods to help sending data through websockets.
29   *
30   * @author maxence
31   */
32  public class WebsocketMessagePreparer {
33  
34      /** logger */
35      private static final Logger logger = LoggerFactory.getLogger(WebsocketMessagePreparer.class);
36  
37      /**
38       * never-called private constructor
39       */
40      private WebsocketMessagePreparer() {
41          throw new UnsupportedOperationException(
42              "This is a utility class and cannot be instantiated");
43      }
44  
45      /**
46       * Get the message that will be sent through the given channel.
47       *
48       * @param messagesByChannel all messagesByChannels
49       * @param key               key to select to correct message
50       *
51       * @return the message
52       */
53      private static WsUpdateMessage getOrCreateWsUpdateMessage(
54          Map<WebsocketChannel, List<WsMessage>> messagesByChannel,
55          WebsocketChannel key
56      ) {
57          logger.trace("GetOrCreate WsUpdateMessge for channel {}", key);
58          if (!messagesByChannel.containsKey(key)) {
59              logger.trace(" -> create channel {}", key);
60              messagesByChannel.put(key, List.of(new WsUpdateMessage()));
61              return (WsUpdateMessage) messagesByChannel.get(key).get(0);
62          } else {
63              List<WsMessage> get = messagesByChannel.get(key);
64              logger.trace(" -> use existing channel {} := {}", key, get);
65              Optional<WsMessage> find = get.stream()
66                  .filter(message -> message instanceof WsUpdateMessage)
67                  .findFirst();
68              if (find.isPresent()) {
69                  logger.trace("   -> use existing message {}", find.get());
70                  return (WsUpdateMessage) find.get();
71              } else {
72                  logger.trace("   -> create emtpy message");
73                  WsUpdateMessage wsUpdateMessage = new WsUpdateMessage();
74                  get.add(wsUpdateMessage);
75                  return wsUpdateMessage;
76              }
77          }
78      }
79  
80      /**
81       * Add entity to the set identified by the channel.
82       *
83       * @param messagesByChannel all sets
84       * @param channel           channel to identify correct set
85       * @param entity            entity to add
86       */
87      private static void addAsUpdated(
88          Map<WebsocketChannel, List<WsMessage>> messagesByChannel,
89          WebsocketChannel channel,
90          WithWebsocketChannels entity) {
91          Collection<WithWebsocketChannels> set = WebsocketMessagePreparer
92              .getOrCreateWsUpdateMessage(messagesByChannel, channel).getUpdated();
93          logger.trace("Add {} to updated set {}", entity, set);
94          set.add(entity);
95          if (logger.isTraceEnabled()) {
96              set.forEach(e -> {
97                  logger.trace("Entity: {}", e);
98                  logger.trace("Entity hashCode: {}", e.hashCode());
99                  logger.trace("Entity equals new: {}", e.equals(entity));
100             });
101         }
102     }
103 
104     /**
105      * Add index entry to the set identified by the channel.
106      *
107      * @param byChannels all sets
108      * @param channel    channel to identify correct set
109      * @param entry      entry to add
110      */
111     private static void addAsDeleted(Map<WebsocketChannel, List<WsMessage>> byChannels,
112         WebsocketChannel channel,
113         IndexEntry entry) {
114         Collection<IndexEntry> set = WebsocketMessagePreparer
115             .getOrCreateWsUpdateMessage(byChannels, channel)
116             .getDeleted();
117         logger.trace("Add {} to deleted set {}", entry, set);
118         set.add(entry);
119     }
120 
121     /**
122      * Prepare all WsUpdateMessage.
123      *
124      * @param userDao     provide userDao to resolve nested channels
125      * @param teamDao     provide teamDao to resolve nested channels
126      * @param cardTypeDao provide cardTypeDao to resolve nested channels
127      * @param projectDao  provide projectDao to resolve nested channels
128      * @param updated     set of created/updated entities
129      * @param deleted     set of just destroyed-entities index entry
130      *
131      * @return the precomputed messagesByChannels
132      *
133      * @throws EncodeException if creating JSON messagesByChannels failed
134      */
135     public static PrecomputedWsMessages prepareWsMessage(
136         UserDao userDao,
137         TeamMemberDao teamDao,
138         CardTypeDao cardTypeDao,
139         ProjectDao projectDao,
140         Set<WithWebsocketChannels> updated,
141         Set<IndexEntry> deleted
142     ) throws EncodeException {
143         Map<WebsocketChannel, List<WsMessage>> messagesByChannel = new HashMap<>();
144         logger.debug("Prepare WsMessage. Update:{}; Deleted:{}", updated, deleted);
145 
146         updated.forEach(object -> {
147             logger.trace("Process updated entity {}", object);
148             object.getChannelsBuilder().computeChannels(userDao, teamDao, cardTypeDao, projectDao)
149                 .forEach(channel -> {
150                     addAsUpdated(messagesByChannel, channel, object);
151                 });
152         });
153 
154         deleted.forEach(object -> {
155             logger.trace("Process deleted entry {}", object);
156             object.getChannelsBuilder().computeChannels(userDao, teamDao, cardTypeDao, projectDao)
157                 .forEach(
158                     channel -> {
159                         addAsDeleted(messagesByChannel, channel, object);
160                     });
161         });
162 
163         return PrecomputedWsMessages.build(messagesByChannel);
164     }
165 
166     /**
167      * Prepare one message on admin channel
168      *
169      * @param userDao provide userDao to resolve nested channels
170      * @param message the message
171      *
172      * @return the precomputedMessage
173      *
174      * @throws EncodeException if json-encoding failed
175      */
176     public static PrecomputedWsMessages prepareWsMessageForAdmins(
177         UserDao userDao,
178         WsMessage message
179     ) throws EncodeException {
180         return prepareWsMessage(userDao, null, null, null, new ForAdminChannelsBuilder(), message);
181     }
182 
183     /**
184      * Prepare one message for many channels
185      *
186      * @param userDao        provide userDao to resolve nested channels
187      * @param teamDao        provide teamDao to resolve nested channels
188      * @param cardTypeDao    provide cardTypeDao to resolve nested channels
189      * @param projectDao     provide projectDao to resolve nested channels
190      * @param channelBuilder the channel builder that defines which channels must be used
191      * @param message        the message
192      * @return the precomputedMessage
193      * @throws EncodeException if json-encoding failed
194      */
195     public static PrecomputedWsMessages prepareWsMessage(
196         UserDao userDao,
197         TeamMemberDao teamDao,
198         CardTypeDao cardTypeDao,
199         ProjectDao projectDao,
200         ChannelsBuilder channelBuilder,
201         WsMessage message
202     ) throws EncodeException {
203         Map<WebsocketChannel, List<WsMessage>> messagesByChannel = new HashMap<>();
204 
205         channelBuilder.computeChannels(userDao, teamDao, cardTypeDao, projectDao).forEach(channel -> {
206             messagesByChannel.put(channel, List.of(message));
207         });
208 
209         return PrecomputedWsMessages.build(messagesByChannel);
210     }
211 
212 }