// --------------------------------------------------------------------------- // jWebSocket - Channel // Copyright (c) 2010 Innotrade GmbH, jWebSocket.org // --------------------------------------------------------------------------- // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 3 of the License, or (at your // option) any later version. // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for // more details. // You should have received a copy of the GNU Lesser General Public License along // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>. // --------------------------------------------------------------------------- package org.jwebsocket.plugins.channels; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.jwebsocket.async.IOFuture; import org.jwebsocket.config.xml.ChannelConfig; import org.jwebsocket.security.Right; import org.jwebsocket.security.Rights; import org.jwebsocket.security.SecurityFactory; import org.jwebsocket.token.Token; /** * Channel class represents the data channel which is used by the * <tt>Publisher</tt> to publish the data and the number of <tt>Subscriber</tt> * 's can subscribe to the given channel to receive the data stream through the * channel as soon as it is available to the channel via publisher. * * Channel can be of 3 types: * * 1. System Channel - The channels which are and can only be initialized and * started by the jWebSocket components and are used by it for providing system * level information are called system channel. Examples can be * <tt>LoggerChannel</tt> for streaming server logs to the client, * <tt>AdminChannel<tt> * to stream the admin level read only information etc.. * * 2. Private Channel - These are the channels that can be registered, initialized * and started by user at configuration time using <tt>jWebSocket.xml</tt> or * runtime. But to subscribe to this channel the user or client should have * valid <tt>api_key</tt> or rights. * * 3. Public Channel - Same as private channel except anyone can subscribe to * this channel without the use of <tt>access_key</tt> or irrespective of the * roles and rights. * * Also <tt>CopyOnWriteArrayList</tt> has been used for the list of subscribers, * publishers and channel listeners for the concurrent access. Although it is * expensive but considering the fact that number of traversal for broadcasting * data or callback on listeners on events would be more than insertion and * removal. * * @author puran * @version $Id$ */ public final class Channel implements ChannelLifeCycle { private String mId; private String mName; private boolean mIsPrivate; private boolean mIsSystem; private String mSecretKey; private String mAccessKey; private long mCreatedDate; private String mOwner; private volatile boolean mAuthenticated = false; private List<Subscriber> mSubscribers; private List<Publisher> mPublishers; private ChannelState mState = ChannelState.STOPPED; private List<ChannelListener> mChannelListeners; public enum ChannelState { STOPPED(0), INITIALIZED(1), STARTED(2), SUSPENDED(3); private int value; ChannelState(int value) { this.value = value; } public int getValue() { return value; } } /** * Initialize the new channel but it doesn't start. * * @param config * the channel config */ public Channel(ChannelConfig config) { this.mId = config.getId(); this.mName = config.getName(); this.mIsPrivate = config.isPrivateChannel(); this.mIsSystem = config.isSystemChannel(); this.mSecretKey = config.getSecretKey(); this.mAccessKey = config.getAccessKey(); this.mOwner = config.getOwner(); this.mCreatedDate = System.currentTimeMillis(); this.mState = ChannelState.INITIALIZED; this.mAuthenticated = false; } public Channel(String aId, String aName, int aSubscriberCount, boolean aPrivateChannel, boolean aSystemChannel, String aSecretKey, String aAccessKey, String aOwner, long aCreatedDate, ChannelState aState, List<Subscriber> aSubscribers, List<Publisher> aPublishers) { this.mId = aId; this.mName = aName; this.mIsPrivate = aPrivateChannel; this.mIsSystem = aSystemChannel; this.mSecretKey = aSecretKey; this.mAccessKey = aAccessKey; this.mOwner = aOwner; this.mCreatedDate = aCreatedDate; this.mSubscribers = aSubscribers; this.mState = aState; } /** * Returns the channel unique id. * * @return the id */ public String getId() { return mId; } public String getName() { return mName; } public int getSubscriberCount() { return mSubscribers.size(); } public boolean isPrivateChannel() { return mIsPrivate; } /** * @return the systemChannel */ public boolean isSystemChannel() { return mIsSystem; } /** * @return the secretKey */ public String getSecretKey() { return mSecretKey; } /** * @return the accessKey */ public String getAccessKey() { return mAccessKey; } /** * @return the createdDate */ public long getCreatedDate() { return mCreatedDate; } /** * @return the owner */ public String getOwner() { return mOwner; } /** * Returns the unmodifiable list of all the subscribers to this channel * * @return the list of subscribers */ public List<Subscriber> getSubscribers() { return Collections.unmodifiableList(mSubscribers); } /** * Set the subscribers to this channel. Note that this method simply * replaces the existing list of subscribers. * * @param aSubscribers * the list of subscribers */ public void setSubscribers(List<Subscriber> aSubscribers) { this.mSubscribers = aSubscribers; } /** * @return the publishers who is currently publishing to this channel */ public List<Publisher> getPublishers() { return Collections.unmodifiableList(mPublishers); } /** * @param aPublishers * the publishers to set */ public void setPublishers(List<Publisher> aPublishers) { if (this.mPublishers == null) { this.mPublishers = new CopyOnWriteArrayList<Publisher>(); } this.mPublishers = aPublishers; } /** * Add the publisher to the list of publishers. * * @param aPublisher * the publisher to add */ public void addPublisher(Publisher aPublisher) { if (this.mPublishers == null) { this.mPublishers = new CopyOnWriteArrayList<Publisher>(); } this.mPublishers.add(aPublisher); } /** * Subscribe to this channel * * @param aSubscriber * the subscriber which wants to subscribe */ public void subscribe(Subscriber aSubscriber, ChannelManager aChannelManager) { if (this.mSubscribers == null) { this.mSubscribers = new CopyOnWriteArrayList<Subscriber>(); } if (!mSubscribers.contains(aSubscriber)) { mSubscribers.add(aSubscriber); aSubscriber.addChannel(this.getId()); // persist the subscriber aChannelManager.storeSubscriber(aSubscriber); if (mChannelListeners != null) { for (ChannelListener lListener : mChannelListeners) { try { lListener.subscribed(this, aSubscriber); } catch (Exception es) { // trap for any exception so that if any of the // listener implementation fails or throws exception // we continue with others. } } } } } /** * Unsubscribe from this channel, and updates the channel store information * * @param aSubscriber * the subscriber to unsubscribe * @param aChannelManager * the channel manager */ public void unsubscribe(Subscriber aSubscriber, ChannelManager aChannelManager) { if (this.mSubscribers == null) { return; } if (mSubscribers.contains(aSubscriber)) { mSubscribers.remove(aSubscriber); // Alex: Also remove from persistent storage. aChannelManager.removeSubscriber(aSubscriber); if (mChannelListeners != null) { for (ChannelListener listener : mChannelListeners) { listener.unsubscribed(this, aSubscriber); } } } } /** * Sends the data to the given subscriber. Note that this send operation * will block the current thread until the send operation is complete. for * asynchronous send operation use <tt>sendAsync</tt> method. * * @param aToken * the token data to send * @param aSubscriber * the target subscriber */ public void send(Token aToken, Subscriber aSubscriber) { aSubscriber.sendToken(aToken); } /** * Sends the data to the given target subscriber asynchronously. * * @param aToken * the token data to send * @param subscriber * the target subscriber * @return the future object to keep track of send operation */ public IOFuture sendAsync(Token aToken, Subscriber aSubscriber) { return aSubscriber.sendTokenAsync(aToken); } /** * broadcasts data to the subscribers asynchronously. It performs the * concurrent broadcast to all the subscribers and wait for the all the * broadcast task to complete only for 1 second maximum. * * @param aToken * the token data for the subscribers */ public void broadcastToken(final Token aToken) { // Added by Alex: If no subscribers exist do nothing! if (mSubscribers != null && mSubscribers.size() > 0) { ExecutorService executor = Executors.newCachedThreadPool(); for (final Subscriber subscriber : mSubscribers) { executor.submit(new Runnable() { @Override public void run() { subscriber.sendTokenAsync(aToken); } }); } try { executor.awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } /** * Returns the channel state * * @return the state */ public ChannelState getState() { return mState; } /** * Register the channel listener to the list of listeners * * @param aChannelListener * the channel listener to register */ public void registerListener(ChannelListener aChannelListener) { if (mChannelListeners == null) { mChannelListeners = new CopyOnWriteArrayList<ChannelListener>(); } mChannelListeners.add(aChannelListener); } public void removeListener(ChannelListener aChannelListener) { if (mChannelListeners != null) { mChannelListeners.remove(aChannelListener); } } @Override public void init() { this.mState = ChannelState.INITIALIZED; } @Override public void start(final String aUser) throws ChannelLifeCycleException { if (this.mState == ChannelState.STARTED) { throw new ChannelLifeCycleException( "Channel '" + this.getName() + "' is already started"); } if (!SecurityFactory.isValidUser(aUser)) { throw new ChannelLifeCycleException( "Cannot start the channel '" + this.getName() + "' for invalid user login '" + aUser + "'"); } else { Rights lRights = SecurityFactory.getUserRights(aUser); Right lRight = lRights.get("org.jwebsocket.plugins.channel.start"); if (lRight == null) { throw new ChannelLifeCycleException("User '" + aUser + "' does not have rights to start the channel"); } else { // verify the owner if (!this.getOwner().equals(aUser)) { throw new ChannelLifeCycleException("User '" + aUser + "' is not a owner of this channel," + "Only owner of the channel can start"); } this.mAuthenticated = true; } } this.mState = ChannelState.STARTED; final Channel lChannel = this; if (mChannelListeners != null) { ExecutorService lPool = Executors.newCachedThreadPool(); for (final ChannelListener lListener : mChannelListeners) { lPool.submit(new Runnable() { @Override public void run() { lListener.channelStarted(lChannel, aUser); } }); } lPool.shutdown(); } } @Override public void suspend(final String aUser) throws ChannelLifeCycleException { if (this.mState == ChannelState.SUSPENDED) { throw new ChannelLifeCycleException("Channel:[" + this.getName() + "] is already suspended"); } if (!SecurityFactory.isValidUser(aUser) && !mAuthenticated) { throw new ChannelLifeCycleException("Cannot suspend the channel:[" + this.getName() + "] for invalid user login [" + aUser + "]"); } else { Rights rights = SecurityFactory.getUserRights(aUser); Right right = rights.get("org.jwebsocket.plugins.channel.suspend"); if (right == null) { throw new ChannelLifeCycleException("User:[" + aUser + "] does not have rights to suspend the channel"); } else { // verify the owner if (!this.getOwner().equals(aUser)) { throw new ChannelLifeCycleException("User:[" + aUser + "] is not a owner of this channel," + "Only owner of the channel can suspend"); } } } this.mState = ChannelState.SUSPENDED; final Channel channel = this; if (mChannelListeners != null) { ExecutorService pool = Executors.newCachedThreadPool(); for (final ChannelListener listener : mChannelListeners) { pool.submit(new Runnable() { @Override public void run() { listener.channelSuspended(channel, aUser); } }); } pool.shutdown(); } } @Override public void stop(final String aUser) throws ChannelLifeCycleException { if (this.mState == ChannelState.STOPPED) { throw new ChannelLifeCycleException("Channel:[" + this.getName() + "] is already stopped"); } if (!SecurityFactory.isValidUser(aUser) && !mAuthenticated) { throw new ChannelLifeCycleException("Cannot stop the channel:[" + this.getName() + "] for invalid user login [" + aUser + "]"); } else { Rights rights = SecurityFactory.getUserRights(aUser); Right right = rights.get("org.jwebsocket.plugins.channel.stop"); if (right == null) { throw new ChannelLifeCycleException("User:[" + aUser + "] does not have rights to stop the channel"); } else { // verify the owner if (!this.getOwner().equals(aUser)) { throw new ChannelLifeCycleException("User:[" + aUser + "] is not a owner of this channel," + "Only owner of the channel can stop"); } } } if (this.mState == ChannelState.INITIALIZED || this.mState == ChannelState.STARTED) { this.mState = ChannelState.STOPPED; } final Channel channel = this; if (mChannelListeners != null) { ExecutorService pool = Executors.newCachedThreadPool(); for (final ChannelListener listener : mChannelListeners) { pool.submit(new Runnable() { @Override public void run() { listener.channelStopped(channel, aUser); } }); } pool.shutdown(); } } /** * @param aId * the id to set */ public void setId(String aId) { this.mId = aId; } /** * @param aSecretKey * the secretKey to set */ public void setSecretKey(String aSecretKey) { this.mSecretKey = aSecretKey; } /** * @param aAccessKey * the accessKey to set */ public void setAccessKey(String aAccessKey) { this.mAccessKey = aAccessKey; } /** * @param aOwner * the owner to set */ public void setOwner(String aOwner) { this.mOwner = aOwner; } public boolean isAuthenticated() { return mAuthenticated; } }