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

This is the master POM file for Oracle's Implementation of the JSF 2.2 Specification.

There is a newer version: 2.4.0
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2016 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
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 javax.annotation.PreDestroy;
import javax.enterprise.context.SessionScoped;
import javax.faces.context.FacesContext;
import javax.faces.push.Push;
import javax.faces.view.ViewScoped;
import javax.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