View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.push.cdi;
21  
22  import java.io.Serializable;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import jakarta.annotation.PreDestroy;
29  import jakarta.enterprise.inject.spi.CDI;
30  import jakarta.faces.view.ViewScoped;
31  import org.apache.myfaces.cdi.util.CDIUtils;
32  
33  /**
34   * The purpose of this view scope bean is keep track of the channelTokens used in this view and if the view
35   * is discarded, destroy the websocket sessions associated with the view because they are no longer valid.
36   */
37  @ViewScoped
38  public class WebsocketViewBean implements Serializable
39  {
40      
41      /**
42       * This map hold all tokens that are related to the current scope. 
43       * This map use as key channel and as value channelTokens
44       */
45      private Map<String, List<WebsocketChannel> > channelTokenListMap = 
46              new HashMap<String, List<WebsocketChannel> >(2);
47      
48      /**
49       * This map hold all tokens related to the current view. The reason to do this is the connections must follow
50       * the same rules the view has, so if a view is disposed, all related websocket sessions must be disposed too
51       * on the server, and in that way we can avoid memory leaks. This bean has a PreDestroy annotation to dispose all
52       * related websocket sessions.
53       * 
54       * This map also enforces a rule that there is only one websocket token pero combination of channel, scope and user
55       * per view. In that way, the token can be used to identify on the client if a websocket initialization request
56       * can share a websocket connection or not, simplifying code design.
57       */
58      private Map<String, WebsocketChannelMetadata> tokenList = new HashMap<String, WebsocketChannelMetadata>(2);
59      
60      public void registerToken(String token, WebsocketChannelMetadata metadata)
61      {
62          tokenList.put(token, metadata);
63      }
64      
65      public void registerWebsocketSession(String token, WebsocketChannelMetadata metadata)
66      {
67          if ("view".equals(metadata.getScope()))
68          {
69              channelTokenListMap.putIfAbsent(metadata.getChannel(), new ArrayList<WebsocketChannel>(1));
70              channelTokenListMap.get(metadata.getChannel()).add(new WebsocketChannel(
71                      token, metadata));
72          }
73      }
74      
75      public boolean isSessionTokenValid(String token)
76      {
77          boolean valid = false;
78          for (List<WebsocketChannel> chlist : channelTokenListMap.values())
79          {
80              if (chlist.contains(token))
81              {
82                  valid = true;
83                  break;
84              }
85          }
86          return valid;
87      }
88      
89      /**
90       * Indicate if the channel mentioned is valid for view scope.
91       * 
92       * A channel is valid if there is at least one token that represents a valid connection to this channel.
93       * 
94       * @param channel
95       * @return 
96       */
97      public boolean isChannelAvailable(String channel)
98      {
99          return channelTokenListMap.containsKey(channel);
100     }
101     
102     public List<String> getChannelTokensFor(String channel)
103     {
104         List<WebsocketChannel> list = channelTokenListMap.get(channel);
105         if (list != null && !list.isEmpty())
106         {
107             List<String> value = new ArrayList<String>(list.size());
108             for (WebsocketChannel md : list)
109             {
110                 value.add(md.getChannelToken());
111             }
112             return value;
113         }
114         return Collections.emptyList();
115     }
116     
117     public String getChannelToken(WebsocketChannelMetadata metadata)
118     {
119         if (!metadata.isConnected())
120         {
121             // Always generate a connection
122             return null;
123         }
124         String token = null;
125         for (Map.Entry<String, WebsocketChannelMetadata> entry : tokenList.entrySet())
126         {
127             if (metadata.equals(entry.getValue()))
128             {
129                 token = entry.getKey();
130                 break;
131             }
132         }
133         return token;
134     }
135     
136     public <S extends Serializable> List<String> getChannelTokensFor(String channel, S user)
137     {
138         List<WebsocketChannel> list = channelTokenListMap.get(channel);
139         if (list != null && !list.isEmpty())
140         {
141             List<String> value = new ArrayList<String>(list.size());
142             for (WebsocketChannel md : list)
143             {
144                 if (user.equals(md.getUser()))
145                 {
146                     value.add(md.getChannelToken());
147                 }
148             }
149             return value;
150         }
151         return null;
152     }
153     
154     @PreDestroy
155     public void destroy()
156     {
157         WebsocketSessionBean sessionHandler = CDIUtils.lookup(CDI.current().getBeanManager(), 
158                 WebsocketSessionBean.class);
159         if (sessionHandler != null)
160         {
161             for (String token : tokenList.keySet())
162             {
163                 sessionHandler.destroyChannelToken(token);
164             }
165         }
166         
167         for (String token : tokenList.keySet())
168         {
169             WebsocketApplicationSessionHolder.removeSession(token);
170         }
171         channelTokenListMap.clear();
172         tokenList.clear();
173     }
174 }