Java tutorial
/* * 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.wicketstuff.push.cometd; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.wicket.Component; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.protocol.http.WebApplication; import org.cometd.bayeux.server.BayeuxServer; import org.cometd.bayeux.server.BayeuxServer.ChannelListener; import org.cometd.bayeux.server.BayeuxServer.SessionListener; import org.cometd.bayeux.server.BayeuxServer.SubscriptionListener; import org.cometd.bayeux.server.ConfigurableServerChannel; import org.cometd.bayeux.server.ServerChannel; import org.cometd.bayeux.server.ServerSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wicketstuff.push.AbstractPushService; import org.wicketstuff.push.AbstractPushServiceRef; import org.wicketstuff.push.IPushChannel; import org.wicketstuff.push.IPushEventHandler; import org.wicketstuff.push.IPushNode; import org.wicketstuff.push.IPushNodeDisconnectedListener; import org.wicketstuff.push.IPushService; import org.wicketstuff.push.IPushServiceRef; import org.wicketstuff.push.PushChannel; /** * Cometd based implementation of {@link IPushService}. * <p> * This implementation relies on cometd for updating the page, but actually uses regular cometd * events, that will trigger a Wicket AJAX call to get the page actually refreshed using regular * Wicket AJAX mechanisms. * <p> * This mean that each time an event is published, a new connection is made to the server to get the * actual page update performed by the {@link IPushEventHandler}. * * @author Xavier Hanin * @author Rodolfo Hansen * @author <a href="http://sebthom.de/">Sebastian Thomschke</a> */ public class CometdPushService extends AbstractPushService { private static final class PushNodeState<EventType> { protected final CometdPushNode<EventType> node; protected List<CometdPushEventContext<EventType>> queuedEvents = new ArrayList<CometdPushEventContext<EventType>>( 2); protected final Object queuedEventsLock = new Object(); protected PushNodeState(final CometdPushNode<EventType> node) { this.node = node; } } private static Logger LOG = LoggerFactory.getLogger(CometdPushService.class); private static final Map<WebApplication, CometdPushService> INSTANCES = new WeakHashMap<WebApplication, CometdPushService>(); private static final IPushServiceRef<CometdPushService> PUSH_SERVICE_REF = new AbstractPushServiceRef<CometdPushService>() { private static final long serialVersionUID = 1L; @Override protected CometdPushService lookupService() { return CometdPushService.get(); } }; public static CometdPushService get() { return get(WebApplication.get()); } public static CometdPushService get(final WebApplication application) { synchronized (INSTANCES) { CometdPushService service = INSTANCES.get(application); if (service == null) { service = new CometdPushService(application); INSTANCES.put(application, service); } return service; } } /** * @return a serializable service reference */ public static IPushServiceRef<CometdPushService> getRef() { return PUSH_SERVICE_REF; } private final ConcurrentMap<String, List<CometdPushNode<?>>> _nodesByCometdChannelId = new ConcurrentHashMap<String, List<CometdPushNode<?>>>(); private final ConcurrentMap<CometdPushNode<?>, PushNodeState<?>> _nodeStates = new ConcurrentHashMap<CometdPushNode<?>, PushNodeState<?>>(); private final WebApplication _application; private BayeuxServer _bayeux; private CometdPushService(final WebApplication application) { _application = application; _getBayeuxServer().addListener(new ChannelListener() { @Override public void channelAdded(final ServerChannel channel) { LOG.debug("Cometd channel added. channel={}", channel); } @Override public void channelRemoved(final String channelId) { LOG.debug("Cometd channel removed. channel={}", channelId); } @Override public void configureChannel(final ConfigurableServerChannel channel) { // nothing } }); _getBayeuxServer().addListener(new SessionListener() { @Override public void sessionAdded(final ServerSession session) { LOG.debug("Cometd server session added. session={}", session); } @Override public void sessionRemoved(final ServerSession session, final boolean timedout) { LOG.debug("Cometd server session removed. session={}", session); } }); _getBayeuxServer().addListener(new SubscriptionListener() { @Override public void subscribed(final ServerSession session, final ServerChannel channel) { LOG.debug("Cometd channel subscribe. session={} channel={}", session, channel); } @Override public void unsubscribed(final ServerSession session, final ServerChannel channel) { LOG.debug("Cometd channel unsubscribe. session={}, channel={}", session, channel); final List<CometdPushNode<?>> nodes = _nodesByCometdChannelId.remove(channel.getId()); if (nodes != null) for (final CometdPushNode<?> node : nodes) _onDisconnect(node); } }); } private CometdPushBehavior _findPushBehaviour(final Component component) { for (final Behavior behavior : component.getBehaviors()) if (behavior instanceof CometdPushBehavior) return (CometdPushBehavior) behavior; return null; } private synchronized final BayeuxServer _getBayeuxServer() { if (_bayeux == null) _bayeux = (BayeuxServer) _application.getServletContext().getAttribute(BayeuxServer.ATTRIBUTE); return _bayeux; } private ServerChannel _getBayeuxServerChannel(final CometdPushNode<?> node) { return _getBayeuxServer().getChannel(node.getCometdChannelId()); } private <EventType> void _onConnect(final CometdPushNode<EventType> node) { _nodeStates.put(node, new PushNodeState<EventType>(node)); List<CometdPushNode<?>> nodes = _nodesByCometdChannelId.get(node.getCometdChannelId()); if (nodes == null) { // create a new list final List<CometdPushNode<?>> newList = new ArrayList<CometdPushNode<?>>(2); // put the list, a list object is returned in case it was put in the meantime nodes = _nodesByCometdChannelId.putIfAbsent(node.getCometdChannelId(), newList); if (nodes == null) newList.add(node); else nodes.add(node); } } private void _onDisconnect(final CometdPushNode<?> node) { if (_nodeStates.remove(node) != null) { LOG.debug("Cometd push node {} disconnected.", node); disconnectFromAllChannels(node); for (final IPushNodeDisconnectedListener listener : disconnectListeners) try { listener.onDisconnect(node); } catch (final RuntimeException ex) { LOG.error("Failed to notify " + listener, ex); } } } /** * {@inheritDoc} */ @Override public <EventType> CometdPushNode<EventType> installNode(final Component component, final IPushEventHandler<EventType> handler) { CometdPushBehavior behavior = _findPushBehaviour(component); if (behavior == null) { behavior = new CometdPushBehavior(); component.add(behavior); } final CometdPushNode<EventType> node = behavior.addNode(handler); _onConnect(node); return node; } /** * {@inheritDoc} */ @Override public boolean isConnected(final IPushNode<?> node) { if (node instanceof CometdPushNode) return _getBayeuxServerChannel((CometdPushNode<?>) node) != null; LOG.warn("Unsupported push node type {}", node); return false; } boolean isWebSocketTransportAvailable() { return CometdPushService.get()._getBayeuxServer().getAllowedTransports().contains("websocket"); } @SuppressWarnings("unchecked") <EventType> List<CometdPushEventContext<EventType>> pollEvents(final CometdPushNode<EventType> node) { final PushNodeState<EventType> state = (PushNodeState<EventType>) _nodeStates.get(node); if (state == null) { LOG.debug("Reconnecting push node {}...", node); _onConnect(node); return Collections.EMPTY_LIST; } if (state.queuedEvents.size() == 0) return Collections.EMPTY_LIST; synchronized (state.queuedEventsLock) { final List<CometdPushEventContext<EventType>> events = state.queuedEvents; state.queuedEvents = new ArrayList<CometdPushEventContext<EventType>>(2); return events; } } /** * {@inheritDoc} */ @Override public <EventType> void publish(final IPushChannel<EventType> channel, final EventType event) { if (channel == null) throw new IllegalArgumentException("Argument [channel] must not be null"); final Set<IPushNode<?>> pnodes = nodesByChannels.get(channel); if (pnodes == null) throw new IllegalArgumentException("Unknown channel " + channel); final CometdPushEventContext<EventType> ctx = new CometdPushEventContext<EventType>(event, channel, this); // publish the event to all registered nodes for (final IPushNode<?> pnode : pnodes) { @SuppressWarnings("unchecked") final CometdPushNode<EventType> node = (CometdPushNode<EventType>) pnode; final ServerChannel cchannel = _getBayeuxServerChannel(node); if (cchannel == null) { LOG.warn("No cometd channel found for {}", node); continue; } @SuppressWarnings("unchecked") final PushNodeState<EventType> state = (PushNodeState<EventType>) _nodeStates.get(node); if (state != null) { synchronized (state.queuedEventsLock) { state.queuedEvents.add(ctx); } cchannel.publish(null, "pollEvents", state.node.getCometdChannelEventId()); } } } /** * {@inheritDoc} */ @Override public <EventType> void publish(final IPushNode<EventType> node, final EventType event) { if (node instanceof CometdPushNode) { final ServerChannel cchannel = _getBayeuxServerChannel((CometdPushNode<?>) node); if (cchannel == null) LOG.warn("No cometd channel found for {}", node); else { @SuppressWarnings("unchecked") final PushNodeState<EventType> state = (PushNodeState<EventType>) _nodeStates.get(node); if (state != null) { synchronized (state.queuedEventsLock) { state.queuedEvents.add(new CometdPushEventContext<EventType>(event, null, this)); } cchannel.publish(null, "pollEvents", state.node.getCometdChannelEventId()); } } } else LOG.warn("Unsupported push node type {}", node); } /** * Directly sends JavaScript code to the node via a cometd channel without an additional Wicket * AJAX request roundtrip. */ public <EventType> void publishJavascript(final CometdPushNode<EventType> node, final String javascript) { final ServerChannel channel = _getBayeuxServerChannel(node); if (channel == null) LOG.warn("No cometd channel found for {}", node); else channel.publish(null, "javascript:" + javascript, node.getCometdChannelEventId()); } /** * Directly sends JavaScript code to all nodes listeing to the given push channel via a cometd * channel without an additional Wicket AJAX request roundtrip. */ @SuppressWarnings("unchecked") public <EventType> void publishJavascript(final PushChannel<EventType> channel, final String javascript) { if (channel == null) throw new IllegalArgumentException("Argument [channel] must not be null"); final Set<IPushNode<?>> pnodes = nodesByChannels.get(channel); if (pnodes == null) throw new IllegalArgumentException("Unknown channel " + channel); // publish the event to all registered nodes for (final IPushNode<?> node : pnodes) publishJavascript((CometdPushNode<EventType>) node, javascript); } /** * {@inheritDoc} */ @Override public void uninstallNode(final Component component, final IPushNode<?> node) { if (node instanceof CometdPushNode) { final CometdPushBehavior behavior = _findPushBehaviour(component); if (behavior == null) return; if (behavior.removeNode(node) == 0) component.remove(behavior); } else LOG.warn("Unsupported push node type {}", node); } }