/* * Copyright 2017 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. */ package org.omnifaces.cdi.push; import static java.lang.String.format; import static org.omnifaces.cdi.push.SocketChannelManager.ESTIMATED_TOTAL_CHANNELS; import static org.omnifaces.util.Ajax.oncomplete; import static org.omnifaces.util.Components.addScriptToBody; import static org.omnifaces.util.Components.forEachComponent; import static org.omnifaces.util.Faces.getViewRoot; import static org.omnifaces.util.FacesLocal.getViewAttribute; import static org.omnifaces.util.FacesLocal.isAjaxRequestWithPartialRendering; import static org.omnifaces.util.FacesLocal.setViewAttribute; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import javax.faces.event.PostAddToViewEvent; import javax.faces.event.PreRenderViewEvent; import javax.faces.event.SystemEvent; import javax.faces.event.SystemEventListener; import org.omnifaces.util.Callback; /** * <p> * This JSF listener for {@link UIViewRoot} ensures that the necessary JavaScript code to open or close the * <code>WebSocket</code> is properly rendered. * * @author Bauke Scholtz * @see Socket * @since 2.3 */ public class SocketFacesListener implements SystemEventListener { // Constants ------------------------------------------------------------------------------------------------------ private static final String SCRIPT_OPEN = "OmniFaces.Push.open('%s');"; private static final String SCRIPT_CLOSE = "OmniFaces.Push.close('%s');"; // Actions -------------------------------------------------------------------------------------------------------- /** * Only listens on {@link UIViewRoot}. */ @Override public boolean isListenerForSource(Object source) { return source instanceof UIViewRoot; } /** * If the socket has just switched the <code>connected</code> attribute, then render either the <code>open()</code> * script or the <code>close()</code> script. During an ajax request with partial rendering, it's added as * <code><eval></code> by partial response writer, else it's just added as a script component with * <code>target="body"</code>. Those scripts will in turn hit {@link SocketEndpoint}. */ @Override public void processEvent(SystemEvent event) { if (!(event instanceof PreRenderViewEvent)) { return; } final FacesContext context = FacesContext.getCurrentInstance(); final Map<String, Boolean> sockets = getSockets(context); forEachComponent(context).ofTypes(Socket.class).invoke(new Callback.WithArgument<Socket>() { @Override public void invoke(Socket socket) { if (!sockets.containsKey(socket.getChannel())) { return; } boolean connected = socket.isRendered() && socket.isConnected(); boolean previouslyConnected = sockets.put(socket.getChannel(), connected); if (connected != previouslyConnected) { String script = format(connected ? SCRIPT_OPEN : SCRIPT_CLOSE, socket.getChannel()); if (isAjaxRequestWithPartialRendering(context)) { oncomplete(script); } else { addScriptToBody(script); } } }}); } // Helpers -------------------------------------------------------------------------------------------------------- /** * Subscribe this socket faces listener to the current view if necessary. */ static void subscribeIfNecessary() { UIViewRoot view = getViewRoot(); List<SystemEventListener> listeners = view.getListenersForEventClass(PostAddToViewEvent.class); if (listeners != null) { for (SystemEventListener listener : listeners) { if (listener instanceof SocketFacesListener) { return; } } } view.subscribeToViewEvent(PreRenderViewEvent.class, new SocketFacesListener()); } /** * Register given socket and returns true if it's new. Note that this method is in first place not invoked when * <code>socket.isRendered()</code> returns <code>false</code>, so this check is not done here. */ static boolean register(FacesContext context, Socket socket) { return getSockets(context).put(socket.getChannel(), socket.isConnected()) == null; } /** * Helper to remember which sockets are initialized on the view. The map key represents the <code>channel</code> * and the map value represents the last known value of the <code>connected</code> attribute. */ private static Map<String, Boolean> getSockets(FacesContext context) { Map<String, Boolean> sockets = getViewAttribute(context, Socket.class.getName()); if (sockets == null) { sockets = new HashMap<>(ESTIMATED_TOTAL_CHANNELS); setViewAttribute(context, Socket.class.getName(), sockets); } return sockets; } }