com.sun.faces.push.WebsocketChannelManager Maven / Gradle / Ivy
Show all versions of jakarta.faces Show documentation
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.faces.push;
import static com.sun.faces.cdi.CdiUtils.getBeanInstance;
import static com.sun.faces.push.WebsocketUserManager.getUserChannels;
import static java.util.Collections.emptyMap;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.context.FacesContext;
import jakarta.faces.push.Push;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
/**
*
* This web socket channel manager holds all application and session scoped web socket channel identifiers registered by
* <f:websocket>
.
*
* @author Bauke Scholtz
* @see Push
* @since 2.3
*/
@SessionScoped
public class WebsocketChannelManager implements Serializable {
// Constants ------------------------------------------------------------------------------------------------------
private static final long serialVersionUID = 1L;
private static final String ERROR_INVALID_SCOPE = "f:websocket 'scope' attribute '%s' does not represent a valid scope. It may not be an EL expression and allowed"
+ " values are 'application', 'session' and 'view', case insensitive. The default is 'application'. When"
+ " 'user' attribute is specified, then scope defaults to 'session' and may not be 'application'.";
private static final String ERROR_DUPLICATE_CHANNEL = "f:websocket channel '%s' is already registered on a different scope. Choose an unique channel name for a"
+ " different channel (or shutdown all browsers and restart the server if you were just testing).";
private static final int ESTIMATED_CHANNELS_PER_APPLICATION = 1;
private static final int ESTIMATED_CHANNELS_PER_SESSION = 1;
private static final int ESTIMATED_CHANNELS_PER_VIEW = 1;
private static final int ESTIMATED_USERS_PER_SESSION = 1;
static final int ESTIMATED_TOTAL_CHANNELS = ESTIMATED_CHANNELS_PER_APPLICATION + ESTIMATED_CHANNELS_PER_SESSION + ESTIMATED_CHANNELS_PER_VIEW;
static final Map EMPTY_SCOPE = emptyMap();
private enum Scope {
APPLICATION, SESSION, VIEW;
static Scope of(String value, Serializable user) {
if (value == null) {
return user == null ? APPLICATION : SESSION;
}
for (Scope scope : values()) {
if (scope.name().equalsIgnoreCase(value) && (user == null || scope != APPLICATION)) {
return scope;
}
}
throw new IllegalArgumentException(String.format(ERROR_INVALID_SCOPE, value));
}
}
// Properties -----------------------------------------------------------------------------------------------------
private static final ConcurrentMap APPLICATION_SCOPE = new ConcurrentHashMap<>(ESTIMATED_CHANNELS_PER_APPLICATION);
private final ConcurrentMap sessionScope = new ConcurrentHashMap<>(ESTIMATED_CHANNELS_PER_SESSION);
private final ConcurrentMap sessionUsers = new ConcurrentHashMap<>(ESTIMATED_USERS_PER_SESSION);
@Inject
private WebsocketSessionManager socketSessions;
@Inject
private WebsocketUserManager socketUsers;
// Actions --------------------------------------------------------------------------------------------------------
/**
* Register given channel on given scope and returns the web socket channel identifier.
*
* @param context The involved faces context.
* @param channel The web socket channel.
* @param scope The web socket scope. Supported values are application
, session
and
* view
, case insensitive. If null
, the default is application
.
* @param user The user object representing the owner of the given channel. If not null
, then scope may not
* be application
.
* @return The web socket URL.
* @throws IllegalArgumentException When the scope is invalid or when channel already exists on a different scope.
*/
@SuppressWarnings("unchecked")
public String register(FacesContext context, String channel, String scope, Serializable user) {
switch (Scope.of(scope, user)) {
case APPLICATION:
return register(context, null, channel, APPLICATION_SCOPE, sessionScope, getViewScope(false));
case SESSION:
return register(context, user, channel, sessionScope, APPLICATION_SCOPE, getViewScope(false));
case VIEW:
return register(context, user, channel, getViewScope(true), APPLICATION_SCOPE, sessionScope);
default:
throw new UnsupportedOperationException();
}
}
@SuppressWarnings("unchecked")
private String register(FacesContext context, Serializable user, String channel, Map targetScope, Map... otherScopes) {
String url = context.getApplication().getViewHandler().getWebsocketURL(context, channel);
if (!targetScope.containsKey(channel)) {
for (Map otherScope : otherScopes) {
if (otherScope.containsKey(channel)) {
throw new IllegalArgumentException(String.format(ERROR_DUPLICATE_CHANNEL, channel));
}
}
String channelId = UUID.randomUUID().toString();
((ConcurrentMap) targetScope).putIfAbsent(channel, channelId);
}
String channelId = targetScope.get(channel);
if (user != null) {
if (!sessionUsers.containsKey(user)) {
sessionUsers.putIfAbsent(user, UUID.randomUUID().toString());
socketUsers.register(user, sessionUsers.get(user));
}
socketUsers.addChannelId(sessionUsers.get(user), channel, channelId);
}
socketSessions.register(channelId);
return url + "?" + channelId;
}
/**
* When current session scope is about to be destroyed, deregister all session scope channels and explicitly close any
* open web sockets associated with it to avoid stale websockets. If any, also deregister session users.
*/
@PreDestroy
protected void deregisterSessionScope() {
for (Entry sessionUser : sessionUsers.entrySet()) {
socketUsers.deregister(sessionUser.getKey(), sessionUser.getValue());
}
socketSessions.deregister(sessionScope.values());
}
// Nested classes -------------------------------------------------------------------------------------------------
/**
* This helps the web socket channel manager to hold view scoped web socket channel identifiers registered by
* <f:websocket>
.
*
* @author Bauke Scholtz
* @see WebsocketChannelManager
* @since 2.3
*/
@ViewScoped
public static class ViewScope implements Serializable {
private static final long serialVersionUID = 1L;
private ConcurrentMap viewScope = new ConcurrentHashMap<>(ESTIMATED_CHANNELS_PER_VIEW);
/**
* When current view scope is about to be destroyed, deregister all view scope channels and explicitly close any open
* web sockets associated with it to avoid stale websockets.
*/
@PreDestroy
protected void deregisterViewScope() {
WebsocketSessionManager.getInstance().deregister(viewScope.values());
}
}
// Internal (static because package private methods in CDI beans are subject to memory leaks) ---------------------
/**
* For internal usage only. This makes it possible to remember session scope channel IDs during injection time of
* {@link WebsocketPushContext} (the CDI session scope is not necessarily active during push send time).
*/
static Map getSessionScope() {
return getBeanInstance(WebsocketChannelManager.class, true).sessionScope;
}
/**
* For internal usage only. This makes it possible to remember view scope channel IDs during injection time of
* {@link WebsocketPushContext} (the CDI view scope is not necessarily active during push send time).
*/
static Map getViewScope(boolean create) {
ViewScope bean = getBeanInstance(ViewScope.class, create);
return bean == null ? EMPTY_SCOPE : bean.viewScope;
}
/**
* For internal usage only. This makes it possible to resolve the session and view scope channel ID during push send
* time in {@link WebsocketPushContext}.
*/
static String getChannelId(String channel, Map sessionScope, Map viewScope) {
String channelId = viewScope.get(channel);
if (channelId == null) {
channelId = sessionScope.get(channel);
if (channelId == null) {
channelId = APPLICATION_SCOPE.get(channel);
}
}
return channelId;
}
// Serialization --------------------------------------------------------------------------------------------------
private void writeObject(ObjectOutputStream output) throws IOException {
output.defaultWriteObject();
// All of below is just in case server restarts with session persistence or failovers/synchronizes to another server.
output.writeObject(APPLICATION_SCOPE);
Map>> sessionUserChannels = new HashMap<>(sessionUsers.size());
for (String userId : sessionUsers.values()) {
sessionUserChannels.put(userId, getUserChannels().get(userId));
}
output.writeObject(sessionUserChannels);
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
input.defaultReadObject();
// Below is just in case server restarts with session persistence or failovers/synchronizes from another server.
APPLICATION_SCOPE.putAll((Map) input.readObject());
Map>> sessionUserChannels = (Map>>) input.readObject();
for (Entry sessionUser : sessionUsers.entrySet()) {
String userId = sessionUser.getValue();
socketUsers.register(sessionUser.getKey(), userId);
getUserChannels().put(userId, sessionUserChannels.get(userId));
}
// Below awkwardness is because WebsocketChannelManager can't be injected in WebsocketSessionManager (CDI session scope
// is not necessarily active during WS session). So it can't just ask us for channel IDs and we have to tell it.
// And, for application scope IDs we make sure they're re-registered after server restart/failover.
socketSessions.register(sessionScope.values());
socketSessions.register(APPLICATION_SCOPE.values());
}
}