You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by lu...@apache.org on 2016/10/22 03:15:34 UTC

svn commit: r1766172 [2/2] - in /myfaces/core/branches/2.3.x: api/src/main/java/javax/faces/application/ api/src/main/java/javax/faces/context/ api/src/main/java/javax/faces/event/ api/src/main/java/javax/faces/push/ impl/src/main/java/org/apache/myfac...

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationBean.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationBean.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationBean.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationBean.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ *
+ */
+@ApplicationScoped
+public class WebsocketApplicationBean
+{
+    
+    /**
+     * This map has as key the channel and as values a list of websocket channels
+     */
+    private Map<String, List<WebsocketChannel> > channelTokenListMap = 
+        new HashMap<String, List<WebsocketChannel> >(2);
+
+    public void registerWebsocketSession(String token, WebsocketChannelMetadata metadata)
+    {
+        if ("application".equals(metadata.getScope()))
+        {
+            channelTokenListMap.putIfAbsent(metadata.getChannel(), new ArrayList<WebsocketChannel>(1));
+            channelTokenListMap.get(metadata.getChannel()).add(new WebsocketChannel(
+                    token, metadata));
+        }
+    }
+    
+    /**
+     * Indicate if the channel mentioned is valid for view scope.
+     * 
+     * A channel is valid if there is at least one token that represents a valid connection to this channel.
+     * 
+     * @param channel
+     * @return 
+     */
+    public boolean isChannelAvailable(String channel)
+    {
+        return channelTokenListMap.containsKey(channel);
+    }
+    
+    public List<String> getChannelTokensFor(String channel)
+    {
+        List<WebsocketChannel> list = channelTokenListMap.get(channel);
+        if (list != null && !list.isEmpty())
+        {
+            List<String> value = new ArrayList<String>(list.size());
+            for (WebsocketChannel md : list)
+            {
+                value.add(md.getChannelToken());
+            }
+            return value;
+        }
+        return Collections.emptyList();
+    }
+    
+    public <S extends Serializable> List<String> getChannelTokensFor(String channel, S user)
+    {
+        List<WebsocketChannel> list = channelTokenListMap.get(channel);
+        if (list != null && !list.isEmpty())
+        {
+            List<String> value = new ArrayList<String>(list.size());
+            for (WebsocketChannel md : list)
+            {
+                if (user.equals(md.getUser()))
+                {
+                    value.add(md.getChannelToken());
+                }
+            }
+            return value;
+        }
+        return null;
+    }
+}

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationSessionHolder.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationSessionHolder.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationSessionHolder.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketApplicationSessionHolder.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+import javax.faces.context.ExternalContext;
+import javax.websocket.Session;
+import org.apache.myfaces.push.WebsocketSessionClusterSerializedRestore;
+import org.apache.myfaces.push.util.Json;
+import org.apache.myfaces.shared.util.ClassUtils;
+import org.apache.myfaces.shared.util.ConcurrentLRUCache;
+import org.apache.myfaces.shared.util.WebConfigParamUtils;
+
+/**
+ *
+ */
+public final class WebsocketApplicationSessionHolder
+{
+    
+    public static final String INIT_PARAM_WEBSOCKET_MAX_CONNECTIONS = "org.apache.myfaces.WEBSOCKET_MAX_CONNECTIONS";
+    
+    public static final Integer INIT_PARAM_WEBSOCKET_MAX_CONNECTIONS_DEFAULT = 5000;
+    
+    private volatile static WeakHashMap<ClassLoader, ConcurrentLRUCache<String, Reference<Session>>> 
+            clWebsocketMap = new WeakHashMap<ClassLoader, ConcurrentLRUCache<String, Reference<Session>>>();
+    
+    private volatile static WeakHashMap<ClassLoader, Queue<String>> clWebsocketRestoredQueue =
+            new WeakHashMap<ClassLoader, Queue<String>>();
+    
+    /**
+     * 
+     * @return 
+     */
+    public static ConcurrentLRUCache<String, Reference<Session>> getWebsocketSessionLRUCache()
+    {
+        ClassLoader cl = ClassUtils.getContextClassLoader();
+        
+        ConcurrentLRUCache<String, Reference<Session>> metadata = (ConcurrentLRUCache<String, Reference<Session>>)
+                WebsocketApplicationSessionHolder.clWebsocketMap.get(cl);
+
+        if (metadata == null)
+        {
+            // Ensure thread-safe put over _metadata, and only create one map
+            // per classloader to hold metadata.
+            synchronized (WebsocketApplicationSessionHolder.clWebsocketMap)
+            {
+                metadata = createWebsocketSessionLRUCache(cl, metadata, INIT_PARAM_WEBSOCKET_MAX_CONNECTIONS_DEFAULT);
+            }
+        }
+
+        return metadata;
+    }
+
+    /**
+     * 
+     * @param context
+     * @return 
+     */
+    public static void initWebsocketSessionLRUCache(ExternalContext context)
+    {
+        ClassLoader cl = ClassUtils.getContextClassLoader();
+        
+        ConcurrentLRUCache<String, Reference<Session>> lruCache = (ConcurrentLRUCache<String, Reference<Session>>)
+                WebsocketApplicationSessionHolder.clWebsocketMap.get(cl);
+
+        int size = WebConfigParamUtils.getIntegerInitParameter(context, 
+                INIT_PARAM_WEBSOCKET_MAX_CONNECTIONS, INIT_PARAM_WEBSOCKET_MAX_CONNECTIONS_DEFAULT);
+
+        ConcurrentLRUCache<String, Reference<Session>> newMetadata = 
+                new ConcurrentLRUCache<String, Reference<Session>>( (size*4+3)/3, size);
+        
+        synchronized (WebsocketApplicationSessionHolder.clWebsocketMap)
+        {
+            if (lruCache == null)
+            {
+                WebsocketApplicationSessionHolder.clWebsocketMap.put(cl, newMetadata);
+                lruCache = newMetadata;
+            }
+            else
+            {
+                // If a Session has been restored, it could be already a lruCache instantiated, so in this case
+                // we need to fill the new one with the old instances, but only the instances that are active
+                // at the moment.
+                for (Map.Entry<String, Reference<Session>> entry : 
+                        lruCache.getLatestAccessedItems(INIT_PARAM_WEBSOCKET_MAX_CONNECTIONS_DEFAULT).entrySet())
+                {
+                    if (entry.getValue() != null && entry.getValue().get() != null && entry.getValue().get().isOpen())
+                    {
+                        newMetadata.put(entry.getKey(), entry.getValue());
+                    }
+                }
+                WebsocketApplicationSessionHolder.clWebsocketMap.put(cl, newMetadata);
+                lruCache = newMetadata;
+            }
+        }
+    }
+
+    private static ConcurrentLRUCache<String, Reference<Session>> createWebsocketSessionLRUCache(
+            ClassLoader cl, ConcurrentLRUCache<String, Reference<Session>> metadata, int size)
+    {
+        metadata = (ConcurrentLRUCache<String, Reference<Session>>) 
+                WebsocketApplicationSessionHolder.clWebsocketMap.get(cl);
+        if (metadata == null)
+        {
+            metadata = new ConcurrentLRUCache<String, Reference<Session>>( (size*4+3)/3, size);
+            WebsocketApplicationSessionHolder.clWebsocketMap.put(cl, metadata);
+        }
+        return metadata;
+    }
+    
+            
+
+    /**
+     * Removes the cached MetadataTarget instances in order to prevent a memory leak.
+     */
+    public static void clearWebsocketSessionLRUCache()
+    {
+        clWebsocketMap.remove(ClassUtils.getContextClassLoader());
+        clWebsocketRestoredQueue.remove(ClassUtils.getContextClassLoader());
+    }
+    
+    public static boolean addOrUpdateSession(String channelToken, Session session)
+    {
+        Reference oldInstance = getWebsocketSessionLRUCache().get(channelToken);
+        if (oldInstance == null)
+        {
+            getWebsocketSessionLRUCache().put(channelToken, new SoftReference<Session>(session));
+        }
+        else if (!session.equals(oldInstance.get()))
+        {
+            getWebsocketSessionLRUCache().put(channelToken, new SoftReference<Session>(session));
+        }
+        return true;
+    }
+
+    /**
+     * Remove the Session associated to the channelToken. This happens when the websocket connection is closed.
+     * Please note the connection can be closed/reopened, so this method should not block another connection using
+     * the same channelToken. To destroy the channel token, WebsocketViewBean is used to destroy the channel token
+     * at view expiration time.
+     * 
+     * @param channelToken
+     * @param session
+     * @return 
+     */
+    public static boolean removeSession(String channelToken)
+    {
+        getWebsocketSessionLRUCache().remove(channelToken);
+        return false;
+    }
+    
+    
+    protected static Set<Future<Void>> send(String channelToken, Object message)
+    {
+        // Before send, we need to check 
+        synchronizeSessionInstances();
+            
+        Set< Future<Void> > results = new HashSet< Future<Void> >(1);
+        Reference<Session> sessionRef = (channelToken != null) ? getWebsocketSessionLRUCache().get(channelToken) : null;
+
+        if (sessionRef != null && sessionRef.get() != null)
+        {
+            String json = Json.encode(message);
+            Session session = sessionRef.get();
+            if (session.isOpen())
+            {
+                send(session, json, results, 0);
+            }
+            else
+            {
+                //If session is not open, remove the session, because a websocket session after is closed cannot
+                //be alive.
+                getWebsocketSessionLRUCache().remove(channelToken);
+            }
+        }
+        return results;
+    }
+
+    private static final String WARNING_TOMCAT_WEB_SOCKET_BOMBED =
+            "Tomcat cannot handle concurrent push messages. A push message has been sent only after %s retries."
+            + " Consider rate limiting sending push messages. For example, once every 500ms.";    
+    
+    private static void send(Session session, String text, Set<Future<Void>> results, int retries)
+    {
+        try
+        {
+            results.add(session.getAsyncRemote().sendText(text));
+
+            if (retries > 0)
+            {
+                Logger.getLogger(WebsocketApplicationSessionHolder.class.getName())
+                        .warning(String.format(WARNING_TOMCAT_WEB_SOCKET_BOMBED, retries));
+            }
+        }
+        catch (IllegalStateException e)
+        {
+            if (isTomcatWebSocketBombed(session, e))
+            {
+                synchronized (session)
+                {
+                    send(session, text, results, retries + 1);
+                }
+            }
+            else
+            {
+                throw e;
+            }
+        }
+    }
+    
+    // Tomcat related -------------------------------------------------------------------------------------------------
+    /**
+     * Returns true if the given WS session is from Tomcat and given illegal state exception is caused by a push bomb
+     * which Tomcat couldn't handle. See also https://bz.apache.org/bugzilla/show_bug.cgi?id=56026 and
+     * https://github.com/omnifaces/omnifaces/issues/234
+     *
+     * @param session The WS session.
+     * @param illegalStateException The illegal state exception.
+     * @return Whether it was Tomcat who couldn't handle the push bomb.
+     * @since 2.5
+     */
+    private static boolean isTomcatWebSocketBombed(Session session, IllegalStateException illegalStateException)
+    {
+        return session.getClass().getName().startsWith("org.apache.tomcat.websocket.")
+                && illegalStateException.getMessage().contains("[TEXT_FULL_WRITING]");
+    }
+    
+    private static void synchronizeSessionInstances()
+    {
+        Queue<String> queue = getRestoredQueue();
+        // The queue is always empty, unless a deserialization of Session instances happen. If that happens, 
+        // we need to ensure all Session instances that were deserialized are on the LRU cache, so all instances
+        // receive the message when a "push" is done.
+        // This is not the ideal, but this is the best we have with the current websocket spec.
+        if (!queue.isEmpty())
+        {
+            // It is necessary to have at least 1 registered Session instance to call getOpenSessions() and get all
+            // instances associated to javax.faces.push Endpoint.
+            Map<String, Reference<Session>> map = getWebsocketSessionLRUCache().getLatestAccessedItems(1);
+            if (map != null && !map.isEmpty())
+            {
+                Reference<Session> ref = map.values().iterator().next();
+                if (ref != null)
+                {
+                    Session s = ref.get();
+                    if (s != null)
+                    {
+                        Set<Session> set = s.getOpenSessions();
+                        
+                        for (Iterator<Session> it = set.iterator(); it.hasNext();)
+                        {
+                            Session instance = it.next();
+                            WebsocketSessionClusterSerializedRestore r = 
+                                    (WebsocketSessionClusterSerializedRestore) instance.getUserProperties().get(
+                                        WebsocketSessionClusterSerializedRestore.WEBSOCKET_SESSION_SERIALIZED_RESTORE);
+                            if (r != null && r.isDeserialized())
+                            {
+                                addOrUpdateSession(r.getChannelToken(), s);
+                            }
+                        }
+                        
+                        // Remove one element from the queue
+                        queue.poll();
+                    }
+                }
+            }
+        }
+    }
+
+    public static Queue<String> getRestoredQueue()
+    {
+        ClassLoader cl = ClassUtils.getContextClassLoader();
+        
+        Queue<String> metadata = (Queue<String>)
+                WebsocketApplicationSessionHolder.clWebsocketRestoredQueue.get(cl);
+
+        if (metadata == null)
+        {
+            // Ensure thread-safe put over _metadata, and only create one map
+            // per classloader to hold metadata.
+            synchronized (WebsocketApplicationSessionHolder.clWebsocketRestoredQueue)
+            {
+                metadata = createRestoredQueue(cl, metadata);
+            }
+        }
+
+        return metadata;
+    }
+    
+    private static Queue<String> createRestoredQueue(ClassLoader cl, Queue<String> metadata)
+    {
+        metadata = (Queue<String>) WebsocketApplicationSessionHolder.clWebsocketRestoredQueue.get(cl);
+        if (metadata == null)
+        {
+            metadata = (Queue<String>) new ConcurrentLinkedQueue<String>();
+            WebsocketApplicationSessionHolder.clWebsocketRestoredQueue.put(cl, metadata);
+        }
+        return metadata;
+    }
+    
+}

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannel.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannel.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannel.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannel.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ *
+ */
+public class WebsocketChannel implements Serializable
+{
+    private String channelToken;
+    
+    private WebsocketChannelMetadata metadata;
+
+    public WebsocketChannel(String channelToken, WebsocketChannelMetadata metadata)
+    {
+        this.channelToken = channelToken;
+        this.metadata = metadata;
+    }
+
+    public WebsocketChannel()
+    {
+    }
+
+    public String getChannelToken()
+    {
+        return channelToken;
+    }
+
+    public String getScope()
+    {
+        return metadata.getScope();
+    }
+
+    public Serializable getUser()
+    {
+        return metadata.getUser();
+    }
+
+    /**
+     * @return the channel
+     */
+    public String getChannel()
+    {
+        return metadata.getChannel();
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 7;
+        hash = 83 * hash + Objects.hashCode(this.channelToken);
+        hash = 83 * hash + Objects.hashCode(this.metadata);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (obj == null)
+        {
+            return false;
+        }
+        if (getClass() != obj.getClass())
+        {
+            return false;
+        }
+        final WebsocketChannel other = (WebsocketChannel) obj;
+        if (!Objects.equals(this.channelToken, other.channelToken))
+        {
+            return false;
+        }
+        if (!Objects.equals(this.metadata, other.metadata))
+        {
+            return false;
+        }
+        return true;
+    }
+    
+}

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelMetadata.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelMetadata.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelMetadata.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelMetadata.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ *
+ */
+public class WebsocketChannelMetadata implements Serializable
+{
+    private String channel;
+    
+    private String scope;
+    
+    private Serializable user;
+    
+    private boolean connected;
+
+    public WebsocketChannelMetadata(String channel, String scope, Serializable user, boolean connected)
+    {
+        this.channel = channel;
+        this.scope = scope;
+        this.user = user;
+        this.connected = connected;
+    }
+
+    public WebsocketChannelMetadata()
+    {
+    }
+
+    /**
+     * @return the channel
+     */
+    public String getChannel()
+    {
+        return channel;
+    }
+
+    /**
+     * @param channel the channel to set
+     */
+    public void setChannel(String channel)
+    {
+        this.channel = channel;
+    }
+
+    /**
+     * @return the scope
+     */
+    public String getScope()
+    {
+        return scope;
+    }
+
+    /**
+     * @param scope the scope to set
+     */
+    public void setScope(String scope)
+    {
+        this.scope = scope;
+    }
+
+    /**
+     * @return the user
+     */
+    public Serializable getUser()
+    {
+        return user;
+    }
+
+    /**
+     * @param user the user to set
+     */
+    public void setUser(Serializable user)
+    {
+        this.user = user;
+    }
+
+    /**
+     * @return the connected
+     */
+    public boolean isConnected()
+    {
+        return connected;
+    }
+
+    /**
+     * @param connected the connected to set
+     */
+    public void setConnected(boolean connected)
+    {
+        this.connected = connected;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 5;
+        hash = 67 * hash + Objects.hashCode(this.channel);
+        hash = 67 * hash + Objects.hashCode(this.scope);
+        hash = 67 * hash + Objects.hashCode(this.user);
+        hash = 67 * hash + (this.connected ? 1 : 0);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (obj == null)
+        {
+            return false;
+        }
+        if (getClass() != obj.getClass())
+        {
+            return false;
+        }
+        final WebsocketChannelMetadata other = (WebsocketChannelMetadata) obj;
+        if (!Objects.equals(this.channel, other.channel))
+        {
+            return false;
+        }
+        if (!Objects.equals(this.scope, other.scope))
+        {
+            return false;
+        }
+        if (!Objects.equals(this.user, other.user))
+        {
+            return false;
+        }
+        if (this.connected != other.connected)
+        {
+            return false;
+        }
+        return true;
+    }
+
+}

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelTokenBuilderBean.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelTokenBuilderBean.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelTokenBuilderBean.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketChannelTokenBuilderBean.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.faces.context.FacesContext;
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
+import org.apache.myfaces.shared.util.WebConfigParamUtils;
+
+/**
+ *
+ */
+@ApplicationScoped
+public class WebsocketChannelTokenBuilderBean
+{
+    /**
+     * Defines how to generate the csrf session token.
+     */
+    @JSFWebConfigParam(since="2.2.0", expectedValues="secureRandom, random", 
+            defaultValue="none", group="state")
+    private static final String RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_PARAM
+            = "org.apache.myfaces.RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN";
+    private static final String RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_PARAM_DEFAULT = "random";
+    
+    private static final String RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_SECURE_RANDOM = "secureRandom";
+    private static final String RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_RANDOM = "random";
+    
+    private CsrfSessionTokenFactory csrfSessionTokenFactory;
+    
+    private boolean initialized;
+    
+    public WebsocketChannelTokenBuilderBean()
+    {
+    }
+    
+    @PostConstruct
+    public void init()
+    {
+        FacesContext facesContext = FacesContext.getCurrentInstance();
+        if (facesContext != null)
+        {
+            internalInit(facesContext);
+        }
+    }
+    
+    private synchronized void internalInit(FacesContext facesContext)
+    {
+        String csrfRandomMode = WebConfigParamUtils.getStringInitParameter(facesContext.getExternalContext(),
+                RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_PARAM, 
+                RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_PARAM_DEFAULT);
+        if (RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN_SECURE_RANDOM.equals(csrfRandomMode))
+        {
+            csrfSessionTokenFactory = new SecureRandomCsrfSessionTokenFactory(facesContext);
+        }
+        else
+        {
+            csrfSessionTokenFactory = new RandomCsrfSessionTokenFactory(facesContext);
+        }        
+        initialized=true;
+    }
+    
+    public String createChannelToken(FacesContext facesContext, String channel)
+    {
+        if (!initialized)
+        {
+            internalInit(facesContext);
+        }
+        return csrfSessionTokenFactory.createCryptographicallyStrongTokenFromSession(facesContext);
+    }
+}

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketSessionBean.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketSessionBean.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketSessionBean.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketSessionBean.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.annotation.PreDestroy;
+import javax.enterprise.context.SessionScoped;
+
+/**
+ * The purpose of this bean is to keep track of the active tokens and Session instances in the current session,
+ * so it can be possible to decide if the token is valid or not for the current session. If the token is not in
+ * application scope and is present in session, it means there was a server restart, so the connection must be
+ * updated (added to application scope).
+ * 
+ */
+@SessionScoped
+public class WebsocketSessionBean implements Serializable
+{
+    
+    /**
+     * This map hold all tokens that are related to the current scope. 
+     * This map use as key channel and as value channelTokens
+     */
+    private Map<String, List<WebsocketChannel> > channelTokenListMap = 
+            new ConcurrentHashMap<String, List<WebsocketChannel> >(2);    
+    
+    /**
+     * This map holds all tokens related to the current session and its associated metadata, that will
+     * be used in the websocket handshake to validate if the incoming request is valid and to store
+     * the user object into the Session object.
+     */
+    private Map<String, WebsocketChannelMetadata> tokenMap = 
+        new ConcurrentHashMap<String, WebsocketChannelMetadata>();
+    
+    public WebsocketSessionBean()
+    {
+    }
+    
+    public void registerToken(String token, WebsocketChannelMetadata metadata)
+    {
+        tokenMap.put(token, metadata);
+    }
+
+    public void registerWebsocketSession(String token, WebsocketChannelMetadata metadata)
+    {
+        if ("session".equals(metadata.getScope()))
+        {
+            channelTokenListMap.putIfAbsent(metadata.getChannel(), new ArrayList<WebsocketChannel>(1));
+            channelTokenListMap.get(metadata.getChannel()).add(new WebsocketChannel(
+                    token, metadata));
+        }
+    }
+    
+    public boolean isTokenValid(String token)
+    {
+        return tokenMap.containsKey(token);
+    }
+    
+    public Serializable getUserFromChannelToken(String channelToken)
+    {
+        if (tokenMap != null)
+        {
+            WebsocketChannelMetadata metadata = tokenMap.get(channelToken);
+            if (metadata != null)
+            {
+                return metadata.getUser();
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * Indicate if the channel mentioned is valid for view scope.
+     * 
+     * A channel is valid if there is at least one token that represents a valid connection to this channel.
+     * 
+     * @param channel
+     * @return 
+     */
+    public boolean isChannelAvailable(String channel)
+    {
+        return channelTokenListMap.containsKey(channel);
+    }
+    
+    public List<String> getChannelTokensFor(String channel)
+    {
+        List<WebsocketChannel> list = channelTokenListMap.get(channel);
+        if (list != null && !list.isEmpty())
+        {
+            List<String> value = new ArrayList<String>(list.size());
+            for (WebsocketChannel md : list)
+            {
+                value.add(md.getChannelToken());
+            }
+            return value;
+        }
+        return Collections.emptyList();
+    }
+    
+    public <S extends Serializable> List<String> getChannelTokensFor(String channel, S user)
+    {
+        List<WebsocketChannel> list = channelTokenListMap.get(channel);
+        if (list != null && !list.isEmpty())
+        {
+            List<String> value = new ArrayList<String>(list.size());
+            for (WebsocketChannel md : list)
+            {
+                if (user.equals(md.getUser()))
+                {
+                    value.add(md.getChannelToken());
+                }
+            }
+            return value;
+        }
+        return null;
+    }
+
+    @PreDestroy
+    public void destroy()
+    {
+        // Since there is an algorithm in place for @PreDestroy and @ViewScoped beans using a session
+        // scope bean and @PreDestroy, there is nothing else to do here. But on session expiration
+        // it is easier to just clear the map. At the end it will not cause any side effects.
+        channelTokenListMap.clear();
+        tokenMap.clear();
+    }
+    
+    public void destroyChannelToken(String channelToken)
+    {
+        String channel = null;
+        for (Map.Entry<String, List<WebsocketChannel>> entry : channelTokenListMap.entrySet())
+        {
+            for (Iterator<WebsocketChannel> it = entry.getValue().iterator(); it.hasNext();)
+            {
+                WebsocketChannel wschannel = it.next();
+                if (channelToken.equals(wschannel.getChannelToken()))
+                {
+                    it.remove();
+                    break;
+                }
+            }
+            if (entry.getValue().isEmpty())
+            {
+                channel = entry.getKey();
+            }
+        }
+        if (channel != null)
+        {
+            channelTokenListMap.remove(channel);
+        }
+        tokenMap.remove(channelToken);
+    }
+}

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketViewBean.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketViewBean.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketViewBean.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/cdi/WebsocketViewBean.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.push.cdi;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.PreDestroy;
+import javax.enterprise.inject.spi.CDI;
+import javax.faces.view.ViewScoped;
+import org.apache.myfaces.cdi.util.CDIUtils;
+
+/**
+ * The purpose of this view scope bean is keep track of the channelTokens used in this view and if the view
+ * is discarded, destroy the websocket sessions associated with the view because they are no longer valid.
+ */
+@ViewScoped
+public class WebsocketViewBean implements Serializable
+{
+    
+    /**
+     * This map hold all tokens that are related to the current scope. 
+     * This map use as key channel and as value channelTokens
+     */
+    private Map<String, List<WebsocketChannel> > channelTokenListMap = 
+            new HashMap<String, List<WebsocketChannel> >(2);
+    
+    /**
+     * This map hold all tokens related to the current view. The reason to do this is the connections must follow
+     * the same rules the view has, so if a view is disposed, all related websocket sessions must be disposed too
+     * on the server, and in that way we can avoid memory leaks. This bean has a PreDestroy annotation to dispose all
+     * related websocket sessions.
+     * 
+     * This map also enforces a rule that there is only one websocket token pero combination of channel, scope and user
+     * per view. In that way, the token can be used to identify on the client if a websocket initialization request
+     * can share a websocket connection or not, simplifying code design.
+     */
+    private Map<String, WebsocketChannelMetadata> tokenList = new HashMap<String, WebsocketChannelMetadata>(2);
+    
+    public void registerToken(String token, WebsocketChannelMetadata metadata)
+    {
+        tokenList.put(token, metadata);
+    }
+    
+    public void registerWebsocketSession(String token, WebsocketChannelMetadata metadata)
+    {
+        if ("view".equals(metadata.getScope()))
+        {
+            channelTokenListMap.putIfAbsent(metadata.getChannel(), new ArrayList<WebsocketChannel>(1));
+            channelTokenListMap.get(metadata.getChannel()).add(new WebsocketChannel(
+                    token, metadata));
+        }
+    }
+    
+    public boolean isSessionTokenValid(String token)
+    {
+        boolean valid = false;
+        for (List<WebsocketChannel> chlist : channelTokenListMap.values())
+        {
+            if (chlist.contains(token))
+            {
+                valid = true;
+                break;
+            }
+        }
+        return valid;
+    }
+    
+    /**
+     * Indicate if the channel mentioned is valid for view scope.
+     * 
+     * A channel is valid if there is at least one token that represents a valid connection to this channel.
+     * 
+     * @param channel
+     * @return 
+     */
+    public boolean isChannelAvailable(String channel)
+    {
+        return channelTokenListMap.containsKey(channel);
+    }
+    
+    public List<String> getChannelTokensFor(String channel)
+    {
+        List<WebsocketChannel> list = channelTokenListMap.get(channel);
+        if (list != null && !list.isEmpty())
+        {
+            List<String> value = new ArrayList<String>(list.size());
+            for (WebsocketChannel md : list)
+            {
+                value.add(md.getChannelToken());
+            }
+            return value;
+        }
+        return Collections.emptyList();
+    }
+    
+    public String getChannelToken(WebsocketChannelMetadata metadata)
+    {
+        if (!metadata.isConnected())
+        {
+            // Always generate a connection
+            return null;
+        }
+        String token = null;
+        for (Map.Entry<String, WebsocketChannelMetadata> entry : tokenList.entrySet())
+        {
+            if (metadata.equals(entry.getValue()))
+            {
+                token = entry.getKey();
+                break;
+            }
+        }
+        return token;
+    }
+    
+    public <S extends Serializable> List<String> getChannelTokensFor(String channel, S user)
+    {
+        List<WebsocketChannel> list = channelTokenListMap.get(channel);
+        if (list != null && !list.isEmpty())
+        {
+            List<String> value = new ArrayList<String>(list.size());
+            for (WebsocketChannel md : list)
+            {
+                if (user.equals(md.getUser()))
+                {
+                    value.add(md.getChannelToken());
+                }
+            }
+            return value;
+        }
+        return null;
+    }
+    
+    @PreDestroy
+    public void destroy()
+    {
+        WebsocketSessionBean sessionHandler = CDIUtils.lookup(CDI.current().getBeanManager(), 
+                WebsocketSessionBean.class);
+        if (sessionHandler != null)
+        {
+            for (String token : tokenList.keySet())
+            {
+                sessionHandler.destroyChannelToken(token);
+            }
+        }
+        
+        for (String token : tokenList.keySet())
+        {
+            WebsocketApplicationSessionHolder.removeSession(token);
+        }
+        channelTokenListMap.clear();
+        tokenList.clear();
+    }
+}
\ No newline at end of file

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/util/Json.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/util/Json.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/util/Json.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/push/util/Json.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,389 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.push.util;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Array;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TimeZone;
+
+/**
+ * A simple JSON encoder.
+ *
+ * @author Arjan Tijms
+ * @author Bauke Scholtz
+ * @see org.omnifaces.util.Json version 1.2 file licensed under ASL v2.0 
+ *      org.omnifaces.util.Utils
+ *      Copyright 2016 OmniFaces and the original author or authors.
+ */
+public final class Json
+{
+    // Constants ------------------------------------------------------------------------------------------------------
+    private static final String ERROR_INVALID_BEAN = "Cannot introspect object of type '%s' as bean.";
+    private static final String ERROR_INVALID_GETTER = "Cannot invoke getter of property '%s' of bean '%s'.";
+
+    // Constructors ---------------------------------------------------------------------------------------------------
+    private Json()
+    {
+        // Hide constructor.
+    }
+
+    // Encode ---------------------------------------------------------------------------------------------------------
+    /**
+     * Encodes the given object as JSON. This supports the standard types {@link Boolean}, {@link Number},
+     * {@link CharSequence} and {@link Date}. If the given object type does not match any of them, then it will attempt
+     * to inspect the object as a javabean whereby the public properties (with public getters) will be encoded as a JS
+     * object. It also supports {@link Collection}s, {@link Map}s and arrays of them, even nested ones. The {@link Date}
+     * is formatted in RFC 1123 format, so you can if necessary just pass it straight to <code>new Date()</code> in
+     * JavaScript.
+     *
+     * @param object The object to be encoded as JSON.
+     * @return The JSON-encoded representation of the given object.
+     * @throws IllegalArgumentException When the given object or one of its properties cannot be inspected as a bean.
+     */
+    public static String encode(Object object)
+    {
+        StringBuilder builder = new StringBuilder();
+        encode(object, builder);
+        return builder.toString();
+    }
+
+    /**
+     * Method allowing tail recursion (prevents potential stack overflow on deeply nested structures).
+     */
+    private static void encode(Object object, StringBuilder builder)
+    {
+        if (object == null)
+        {
+            builder.append("null");
+        }
+        else if (object instanceof Boolean || object instanceof Number)
+        {
+            builder.append(object.toString());
+        }
+        else if (object instanceof CharSequence)
+        {
+            builder.append('"').append(escapeJS(object.toString(), false)).append('"');
+        }
+        else if (object instanceof Date)
+        {
+            builder.append('"').append(formatRFC1123((Date) object)).append('"');
+        }
+        else if (object instanceof Collection<?>)
+        {
+            encodeCollection((Collection<?>) object, builder);
+        }
+        else if (object.getClass().isArray())
+        {
+            encodeArray(object, builder);
+        }
+        else if (object instanceof Map<?, ?>)
+        {
+            encodeMap((Map<?, ?>) object, builder);
+        }
+        else if (object instanceof Class<?>)
+        {
+            encode(((Class<?>) object).getName(), builder);
+        }
+        else
+        {
+            encodeBean(object, builder);
+        }
+    }
+
+    /**
+     * Encode a Java collection as JS array.
+     */
+    private static void encodeCollection(Collection<?> collection, StringBuilder builder)
+    {
+        builder.append('[');
+        int i = 0;
+
+        for (Object element : collection)
+        {
+            if (i++ > 0)
+            {
+                builder.append(',');
+            }
+
+            encode(element, builder);
+        }
+
+        builder.append(']');
+    }
+
+    /**
+     * Encode a Java array as JS array.
+     */
+    private static void encodeArray(Object array, StringBuilder builder)
+    {
+        builder.append('[');
+        int length = Array.getLength(array);
+
+        for (int i = 0; i < length; i++)
+        {
+            if (i > 0)
+            {
+                builder.append(',');
+            }
+
+            encode(Array.get(array, i), builder);
+        }
+
+        builder.append(']');
+    }
+
+    /**
+     * Encode a Java map as JS object.
+     */
+    private static void encodeMap(Map<?, ?> map, StringBuilder builder)
+    {
+        builder.append('{');
+        int i = 0;
+
+        for (Entry<?, ?> entry : map.entrySet())
+        {
+            if (i++ > 0)
+            {
+                builder.append(',');
+            }
+
+            encode(String.valueOf(entry.getKey()), builder);
+            builder.append(':');
+            encode(entry.getValue(), builder);
+        }
+
+        builder.append('}');
+    }
+
+    /**
+     * Encode a Java bean as JS object.
+     */
+    private static void encodeBean(Object bean, StringBuilder builder)
+    {
+        BeanInfo beanInfo;
+
+        try
+        {
+            beanInfo = Introspector.getBeanInfo(bean.getClass());
+        }
+        catch (IntrospectionException e)
+        {
+            throw new IllegalArgumentException(
+                    String.format(ERROR_INVALID_BEAN, bean.getClass()), e);
+        }
+
+        builder.append('{');
+        int i = 0;
+
+        for (PropertyDescriptor property : beanInfo.getPropertyDescriptors())
+        {
+            if (property.getReadMethod() == null || "class".equals(property.getName()))
+            {
+                continue;
+            }
+
+            Object value;
+
+            try
+            {
+                value = property.getReadMethod().invoke(bean);
+            }
+            catch (Exception e)
+            {
+                throw new IllegalArgumentException(
+                        String.format(ERROR_INVALID_GETTER, property.getName(), bean.getClass()), e);
+            }
+
+            if (value == null)
+            {
+                continue;
+            }
+
+            if (i++ > 0)
+            {
+                builder.append(',');
+            }
+
+            encode(property.getName(), builder);
+            builder.append(':');
+            encode(value, builder);
+        }
+
+        builder.append('}');
+    }
+
+    
+    // Escaping/unescaping --------------------------------------------------------------------------------------------
+    
+    private static final int UNICODE_3_BYTES = 0xfff;
+    private static final int UNICODE_2_BYTES = 0xff;
+    private static final int UNICODE_1_BYTE = 0xf;
+    private static final int UNICODE_END_PRINTABLE_ASCII = 0x7f;
+    private static final int UNICODE_BEGIN_PRINTABLE_ASCII = 0x20;    
+    
+    /**
+     * Escapes the given string according the JavaScript code rules. This escapes among others the special characters,
+     * the whitespace, the quotes and the unicode characters. Useful whenever you want to use a Java string variable as
+     * a JavaScript string variable.
+     *
+     * @param string The string to be escaped according the JavaScript code rules.
+     * @param escapeSingleQuote Whether to escape single quotes as well or not. Set to <code>false</code> if you want to
+     * escape it for usage in JSON.
+     * @return The escaped string according the JavaScript code rules.
+     */
+    public static String escapeJS(String string, boolean escapeSingleQuote)
+    {
+        if (string == null)
+        {
+            return null;
+        }
+
+        StringBuilder builder = new StringBuilder(string.length());
+
+        for (char c : string.toCharArray())
+        {
+            if (c > UNICODE_3_BYTES)
+            {
+                builder.append("\\u").append(Integer.toHexString(c));
+            }
+            else if (c > UNICODE_2_BYTES)
+            {
+                builder.append("\\u0").append(Integer.toHexString(c));
+            }
+            else if (c > UNICODE_END_PRINTABLE_ASCII)
+            {
+                builder.append("\\u00").append(Integer.toHexString(c));
+            }
+            else if (c < UNICODE_BEGIN_PRINTABLE_ASCII)
+            {
+                escapeJSControlCharacter(builder, c);
+            }
+            else
+            {
+                escapeJSASCIICharacter(builder, c, escapeSingleQuote);
+            }
+        }
+
+        return builder.toString();
+    }
+
+    private static void escapeJSControlCharacter(StringBuilder builder, char c)
+    {
+        switch (c)
+        {
+            case '\b':
+                builder.append('\\').append('b');
+                break;
+            case '\n':
+                builder.append('\\').append('n');
+                break;
+            case '\t':
+                builder.append('\\').append('t');
+                break;
+            case '\f':
+                builder.append('\\').append('f');
+                break;
+            case '\r':
+                builder.append('\\').append('r');
+                break;
+            default:
+                if (c > UNICODE_1_BYTE)
+                {
+                    builder.append("\\u00").append(Integer.toHexString(c));
+                }
+                else
+                {
+                    builder.append("\\u000").append(Integer.toHexString(c));
+                }
+
+                break;
+        }
+    }
+
+    private static void escapeJSASCIICharacter(StringBuilder builder, char c, boolean escapeSingleQuote)
+    {
+        switch (c)
+        {
+            case '\'':
+                if (escapeSingleQuote)
+                {
+                    builder.append('\\');
+                }
+                builder.append('\'');
+                break;
+            case '"':
+                builder.append('\\').append('"');
+                break;
+            case '\\':
+                builder.append('\\').append('\\');
+                break;
+            case '/':
+                builder.append('\\').append('/');
+                break;
+            default:
+                builder.append(c);
+                break;
+        }
+    }
+    
+    // Dates ----------------------------------------------------------------------------------------------------------
+    
+    private static final String PATTERN_RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz";
+    private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone("GMT");
+    
+    /**
+     * Formats the given {@link Date} to a string in RFC1123 format. This format is used in HTTP headers and in
+     * JavaScript <code>Date</code> constructor.
+     *
+     * @param date The <code>Date</code> to be formatted to a string in RFC1123 format.
+     * @return The formatted string.
+     * @since 1.2
+     */
+    public static String formatRFC1123(Date date)
+    {
+        SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);
+        sdf.setTimeZone(TIMEZONE_GMT);
+        return sdf.format(date);
+    }
+
+    /**
+     * Parses the given string in RFC1123 format to a {@link Date} object.
+     *
+     * @param string The string in RFC1123 format to be parsed to a <code>Date</code> object.
+     * @return The parsed <code>Date</code>.
+     * @throws ParseException When the given string is not in RFC1123 format.
+     * @since 1.2
+     */
+    public static Date parseRFC1123(String string) throws ParseException
+    {
+        SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);
+        return sdf.parse(string);
+    }
+
+}

Modified: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/CoreLibrary.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/CoreLibrary.java?rev=1766172&r1=1766171&r2=1766172&view=diff
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/CoreLibrary.java (original)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/CoreLibrary.java Sat Oct 22 03:15:34 2016
@@ -116,5 +116,8 @@ public final class CoreLibrary extends A
         this.addComponent("viewParam", UIViewParameter.COMPONENT_TYPE, null);
 
         this.addComponent("verbatim", "javax.faces.HtmlOutputText", "javax.faces.Text", VerbatimHandler.class);
+        
+        this.addComponent("websocket", "org.apache.myfaces.WebsocketComponent", 
+                "org.apache.myfaces.WebsocketComponent", WebsocketHandler.class);
     }
 }

Added: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/WebsocketHandler.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/WebsocketHandler.java?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/WebsocketHandler.java (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/view/facelets/tag/jsf/core/WebsocketHandler.java Sat Oct 22 03:15:34 2016
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.view.facelets.tag.jsf.core;
+
+import javax.faces.component.UIComponent;
+import javax.faces.view.facelets.ComponentConfig;
+import javax.faces.view.facelets.ComponentHandler;
+import javax.faces.view.facelets.FaceletContext;
+import javax.faces.view.facelets.TagAttribute;
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletAttribute;
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFFaceletTag;
+import org.apache.myfaces.push.WebsocketComponent;
+import org.apache.myfaces.view.facelets.tag.jsf.ComponentSupport;
+
+/**
+ *
+ */
+@JSFFaceletTag(
+        name = "f:websocket",
+        bodyContent = "empty")
+public class WebsocketHandler extends ComponentHandler/* implements RelocatableResourceHandler*/
+{
+   
+    /**
+     * 
+     */
+    @JSFFaceletAttribute(name = "channel", className = "javax.el.ValueExpression",
+                         deferredValueType = "java.lang.String")
+    private final TagAttribute _channel;
+
+    public WebsocketHandler(ComponentConfig config)
+    {
+        super(config);
+        _channel = getRequiredAttribute("channel");
+    }
+
+    /*
+    public UIComponent findChildByTagId(FaceletContext ctx, UIComponent parent,
+            String id)
+    {
+        //Script with no target and no relocation is possible
+        UIComponent c = ComponentSupport.findChildByTagId(parent, id);
+        if (c == null)
+        {
+            UIViewRoot root = ComponentSupport.getViewRoot(ctx, parent);
+            
+            if (root.getFacetCount() > 0)
+            {
+                Iterator<UIComponent> itr = root.getFacets().values().iterator();
+                while (itr.hasNext() && c == null)
+                {
+                    UIComponent facet = itr.next();
+                    c = ComponentSupport.findChildByTagId(facet, id);
+                }
+            }
+            return c;
+        }
+        else
+        {
+            return c;
+        }
+    }*/
+
+
+    @Override
+    public void onComponentCreated(FaceletContext ctx, UIComponent c,
+            UIComponent parent)
+    {
+        /*
+        UIComponent parentCompositeComponent
+                = FaceletCompositionContext.getCurrentInstance(ctx).getCompositeComponentFromStack();
+        if (parentCompositeComponent != null)
+        {
+            c.getAttributes().put(CompositeComponentELUtils.LOCATION_KEY,
+                    parentCompositeComponent.getAttributes().get(CompositeComponentELUtils.LOCATION_KEY));
+        }*/
+        
+        WebsocketComponent component = (WebsocketComponent) c;
+        component.setInitComponentId(ComponentSupport.getViewRoot(ctx, parent).createUniqueId()+"_wsinit");
+    }
+
+    
+}

Modified: myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/webapp/AbstractFacesInitializer.java
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/webapp/AbstractFacesInitializer.java?rev=1766172&r1=1766171&r2=1766172&view=diff
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/webapp/AbstractFacesInitializer.java (original)
+++ myfaces/core/branches/2.3.x/impl/src/main/java/org/apache/myfaces/webapp/AbstractFacesInitializer.java Sat Oct 22 03:15:34 2016
@@ -59,6 +59,13 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import javax.faces.push.PushContext;
+import javax.websocket.DeploymentException;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
+import org.apache.myfaces.push.EndpointImpl;
+import org.apache.myfaces.push.WebsocketConfigurator;
+import org.apache.myfaces.push.WebsocketFacesInit;
 import org.apache.myfaces.shared.context.ExceptionHandlerImpl;
 import org.apache.myfaces.shared.util.ClassUtils;
 import org.apache.myfaces.spi.ServiceProviderFinder;
@@ -193,6 +200,8 @@ public abstract class AbstractFacesIniti
             _createEagerBeans(facesContext);
 
             _dispatchApplicationEvent(servletContext, PostConstructApplicationEvent.class);
+            
+            initWebsocketIntegration(servletContext, externalContext);
 
             if ( (facesContext.isProjectStage(ProjectStage.Development) || 
                   facesContext.isProjectStage(ProjectStage.Production)) &&
@@ -368,6 +377,11 @@ public abstract class AbstractFacesIniti
         // clear the cache of MetaRulesetImpl in order to prevent a memory leak
         MetaRulesetImpl.clearMetadataTargetCache();
         
+        if (facesContext.getExternalContext().getApplicationMap().containsKey("org.apache.myfaces.push"))
+        {
+            WebsocketFacesInit.clearWebsocketSessionLRUCache(facesContext.getExternalContext());
+        }
+        
         // clear UIViewParameter default renderer map
         try
         {
@@ -720,4 +734,44 @@ public abstract class AbstractFacesIniti
             injectedBeanStorage.clear();
         }
     }
+    
+    protected void initWebsocketIntegration(
+            ServletContext servletContext, ExternalContext externalContext)
+    {
+        Boolean b = WebConfigParamUtils.getBooleanInitParameter(externalContext, 
+                PushContext.ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME);
+        
+        if (Boolean.TRUE.equals(b))
+        {
+            // According to https://tyrus.java.net/documentation/1.13/index/deployment.html section 3.2
+            // we can create a websocket programmatically, getting ServerContainer instance from this location
+            final ServerContainer serverContainer = (ServerContainer) 
+                    servletContext.getAttribute("javax.websocket.server.ServerContainer");
+
+            if (serverContainer != null)
+            {
+                try 
+                {
+                    serverContainer.addEndpoint(ServerEndpointConfig.Builder
+                            .create(EndpointImpl.class, EndpointImpl.JAVAX_FACES_PUSH_PATH)
+                            .configurator(new WebsocketConfigurator(externalContext)).build());
+                    
+                    //Init LRU cache
+                    WebsocketFacesInit.initWebsocketSessionLRUCache(externalContext);
+                    
+                    externalContext.getApplicationMap().put("org.apache.myfaces.push", "true");
+                }
+                catch (DeploymentException e)
+                {
+                    log.log(Level.INFO, "Exception on Initialize Websocket Endpoint: ", e);
+                }
+            }
+            else
+            {
+                log.log(Level.INFO, "f:websocket support enabled but cannot found websocket ServerContainer instance "+
+                        "on current context. If websocket library is available, please include a FakeEndpoint instance "
+                        + "into your code to force enable it (Tyrus users).");
+            }
+        }
+    }
 }

Modified: myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/NOTICE.txt
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/NOTICE.txt?rev=1766172&r1=1766171&r2=1766172&view=diff
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/NOTICE.txt (original)
+++ myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/NOTICE.txt Sat Oct 22 03:15:34 2016
@@ -12,3 +12,6 @@ for the purpose of implementing Facelets
 This software also includes code from Grails Licensed under Apache License, 
 Version 2.0 (org.apache.myfaces.shared.util.StreamCharBuffer)
 Copyright 2009 the original author or authors. Lari Hotari, Sagire Software Oy
+
+This software also includes code from Omnifaces (http://github.com/omnifaces)
+for the purpose of implementing f:websocket tag.
\ No newline at end of file

Added: myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/licenses/omnifaces-LICENSE.txt
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/licenses/omnifaces-LICENSE.txt?rev=1766172&view=auto
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/licenses/omnifaces-LICENSE.txt (added)
+++ myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/licenses/omnifaces-LICENSE.txt Sat Oct 22 03:15:34 2016
@@ -0,0 +1,10 @@
+Copyright 2016 OmniFaces
+ 
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
\ No newline at end of file

Modified: myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension
URL: http://svn.apache.org/viewvc/myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension?rev=1766172&r1=1766171&r2=1766172&view=diff
==============================================================================
--- myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension (original)
+++ myfaces/core/branches/2.3.x/impl/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension Sat Oct 22 03:15:34 2016
@@ -3,3 +3,4 @@ org.apache.myfaces.flow.cdi.FlowBuilderC
 org.apache.myfaces.flow.cdi.FlowScopeCDIExtension
 org.apache.myfaces.cdi.dependent.DependentBeanExtension
 org.apache.myfaces.config.annotation.CdiAnnotationProviderExtension
+org.apache.myfaces.push.cdi.PushContextCDIExtension