org.wicketstuff.push.cometd.CometdPushService Maven / Gradle / Ivy
Show all versions of push-cometd Show documentation
/*
* 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 java.util.concurrent.CopyOnWriteArraySet;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.IBehavior;
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.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.PushChannel;
/**
* Cometd based implementation of {@link IPushService}.
*
* 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.
*
* 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 Sebastian Thomschke
*/
public class CometdPushService extends AbstractPushService
{
private static final class PushNodeState
{
protected final CometdPushNode node;
protected List> queuedEvents = new ArrayList>(
2);
protected final Object queuedEventsLock = new Object();
protected PushNodeState(final CometdPushNode node)
{
this.node = node;
}
}
private static Logger LOG = LoggerFactory.getLogger(CometdPushService.class);
private static final Map INSTANCES = new WeakHashMap();
public static CometdPushService get()
{
return get(WebApplication.get());
}
public static CometdPushService get(final WebApplication application)
{
CometdPushService service = INSTANCES.get(application);
if (service == null)
{
service = new CometdPushService(application);
INSTANCES.put(application, service);
}
return service;
}
private final ConcurrentMap>> _nodesByCometdChannelId = new ConcurrentHashMap>>();
private final ConcurrentMap, PushNodeState>> _nodeStates = new ConcurrentHashMap, PushNodeState>>();
private final Set _disconnectListeners = new CopyOnWriteArraySet();
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> nodes = _nodesByCometdChannelId.remove(channel.getId());
if (nodes != null)
for (final CometdPushNode> node : nodes)
_onDisconnect(node);
}
});
}
private CometdPushBehavior _findPushBehaviour(final Component component)
{
for (final IBehavior 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 void _onConnect(final CometdPushNode node)
{
_nodeStates.put(node, new PushNodeState(node));
List> nodes = _nodesByCometdChannelId.get(node.getCometdChannelId());
if (nodes == null)
{
// create a new list
final List> newList = new ArrayList>(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 void addNodeDisconnectedListener(final IPushNodeDisconnectedListener listener)
{
_disconnectListeners.add(listener);
}
/**
* {@inheritDoc}
*/
@Override
public CometdPushNode installNode(final Component component,
final IPushEventHandler handler)
{
CometdPushBehavior behavior = _findPushBehaviour(component);
if (behavior == null)
{
behavior = new CometdPushBehavior();
component.add(behavior);
}
final CometdPushNode 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;
}
@SuppressWarnings("unchecked")
List> pollEvents(
final CometdPushNode node)
{
final PushNodeState state = (PushNodeState)_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> events = state.queuedEvents;
state.queuedEvents = new ArrayList>(2);
return events;
}
}
/**
* {@inheritDoc}
*/
@Override
public void publish(final IPushChannel channel, final EventType event)
{
if (channel == null)
throw new IllegalArgumentException("Argument [channel] must not be null");
final Set> pnodes = nodesByChannels.get(channel);
if (pnodes == null)
throw new IllegalArgumentException("Unknown channel " + channel);
final CometdPushEventContext ctx = new CometdPushEventContext(event,
channel, this);
// publish the event to all registered nodes
for (final IPushNode> pnode : pnodes)
{
@SuppressWarnings("unchecked")
final CometdPushNode node = (CometdPushNode)pnode;
final ServerChannel cchannel = _getBayeuxServerChannel(node);
if (cchannel == null)
{
LOG.warn("No cometd channel found for {}", node);
continue;
}
@SuppressWarnings("unchecked")
final PushNodeState state = (PushNodeState)_nodeStates.get(node);
if (state != null)
{
synchronized (state.queuedEventsLock)
{
state.queuedEvents.add(ctx);
}
cchannel.publish(null, "pollEvents", state.node.getCometdChannelEventId());
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void publish(final IPushNode 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 state = (PushNodeState)_nodeStates.get(node);
if (state != null)
{
synchronized (state.queuedEventsLock)
{
state.queuedEvents.add(new CometdPushEventContext(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 void publishJavascript(final CometdPushNode 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 void publishJavascript(final PushChannel channel,
final String javascript)
{
if (channel == null)
throw new IllegalArgumentException("Argument [channel] must not be null");
final Set> 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)node, javascript);
}
/**
* {@inheritDoc}
*/
@Override
public void removeNodeDisconnectedListener(final IPushNodeDisconnectedListener listener)
{
_disconnectListeners.remove(listener);
}
/**
* {@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);
}
}