All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.faces.push.WebsocketChannelManager Maven / Gradle / Ivy

Go to download

Jakarta Faces defines an MVC framework for building user interfaces for web applications, including UI components, state management, event handing, input validation, page navigation, and support for internationalization and accessibility.

There is a newer version: 4.1.0
Show newest version
/*
 * 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()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy