// ---------------------------------------------------------------------------
// jWebSocket - WebSocket Token Server (manages JSON, CSV and XML Tokens)
// Copyright (c) 2010 Alexander Schulze, Innotrade GmbH
// ---------------------------------------------------------------------------
// 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.server;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javolution.util.FastMap;
import org.apache.log4j.Logger;
import org.jwebsocket.api.ServerConfiguration;
import org.jwebsocket.api.WebSocketPacket;
import org.jwebsocket.config.JWebSocketServerConstants;
import org.jwebsocket.kit.WebSocketException;
import org.jwebsocket.logging.Logging;
import org.jwebsocket.api.WebSocketPlugIn;
import org.jwebsocket.api.WebSocketConnector;
import org.jwebsocket.api.WebSocketEngine;
import org.jwebsocket.api.WebSocketServerListener;
import org.jwebsocket.async.IOFuture;
import org.jwebsocket.config.JWebSocketCommonConstants;
import org.jwebsocket.filter.TokenFilterChain;
import org.jwebsocket.kit.BroadcastOptions;
import org.jwebsocket.kit.CloseReason;
import org.jwebsocket.kit.FilterResponse;
import org.jwebsocket.listener.WebSocketServerTokenEvent;
import org.jwebsocket.listener.WebSocketServerTokenListener;
import org.jwebsocket.plugins.TokenPlugInChain;
import org.jwebsocket.token.Token;
import org.jwebsocket.token.TokenFactory;
/**
* @author aschulze
* @author jang
*/
public class TokenServer extends BaseServer {
private static Logger mLog = Logging.getLogger(TokenServer.class);
// specify name space for token server
private static final String NS_TOKENSERVER = JWebSocketServerConstants.NS_BASE + ".tokenserver";
// specify shared connector variables
public static final String VAR_IS_TOKENSERVER = NS_TOKENSERVER + ".isTS";
private volatile boolean mIsAlive = false;
private static ExecutorService mCachedThreadPool;
private final static int TIME_OUT_TERMINATION_THREAD = 10;
private int mCorePoolSize;
private int mMaximumPoolSize;
private int mKeepAliveTime;
private int mBlockingQueueSize;
public TokenServer(ServerConfiguration aServerConfig) {
super(aServerConfig);
mPlugInChain = new TokenPlugInChain(this);
mFilterChain = new TokenFilterChain(this);
mFilterChain = new TokenFilterChain(this);
mCorePoolSize = aServerConfig.getThreadPoolConfig().getCorePoolSize();
mMaximumPoolSize = aServerConfig.getThreadPoolConfig().getMaximumPoolSize();
mKeepAliveTime = aServerConfig.getThreadPoolConfig().getKeepAliveTime();
mBlockingQueueSize = aServerConfig.getThreadPoolConfig().getBlockingQueueSize();
}
@Override
public void startServer() throws WebSocketException {
// Create the thread pool.
mCachedThreadPool = new ThreadPoolExecutor(mCorePoolSize, mMaximumPoolSize, mKeepAliveTime, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(mBlockingQueueSize));
mIsAlive = true;
if (mLog.isInfoEnabled()) {
mLog.info("Token server '" + getId() + "' started.");
}
}
@Override
public boolean isAlive() {
// nothing special to do here.
// Token server does not contain any thread or similar.
return mIsAlive;
}
@Override
public void stopServer() throws WebSocketException {
mIsAlive = false;
// Shutdown the thread pool
if (mCachedThreadPool != null) {
if (mLog.isInfoEnabled()) {
mLog.info("Shuting down token server threadPool.");
}
mCachedThreadPool.shutdown(); // Disable new tasks from being
// submitted
try {
// Wait a while for existing tasks to terminate
if (!mCachedThreadPool.awaitTermination(TIME_OUT_TERMINATION_THREAD, TimeUnit.SECONDS)) {
mCachedThreadPool.shutdownNow(); // Cancel currently
// executing tasks
// Wait a while for tasks to respond to being cancelled
if (!mCachedThreadPool.awaitTermination(TIME_OUT_TERMINATION_THREAD, TimeUnit.SECONDS)) {
mLog.error("Pool did not terminate");
mCachedThreadPool.shutdownNow();
}
}
} catch (InterruptedException lEx) {
// (Re-)Cancel if current thread also interrupted
mCachedThreadPool.shutdownNow();
}
}
if (mLog.isInfoEnabled()) {
mLog.info("Token server '" + getId() + "' stopped.");
}
}
/**
* removes a plug-in from the plug-in chain of the server.
*
* @param aPlugIn
*/
public void removePlugIn(WebSocketPlugIn aPlugIn) {
mPlugInChain.removePlugIn(aPlugIn);
}
@Override
public void engineStarted(WebSocketEngine aEngine) {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing engine '" + aEngine.getId() + "' started...");
}
mPlugInChain.engineStarted(aEngine);
}
@Override
public void engineStopped(WebSocketEngine aEngine) {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing engine '" + aEngine.getId() + "' stopped...");
}
mPlugInChain.engineStopped(aEngine);
}
/**
* {@inheritDoc }
*/
@Override
public void connectorStarted(WebSocketConnector aConnector) {
String lFormat = aConnector.getHeader().getFormat();
if ((lFormat != null)
&& (lFormat.equals(JWebSocketCommonConstants.WS_FORMAT_JSON)
|| lFormat.equals(JWebSocketCommonConstants.WS_FORMAT_XML)
|| lFormat.equals(JWebSocketCommonConstants.WS_FORMAT_CSV))) {
aConnector.setBoolean(VAR_IS_TOKENSERVER, true);
if (mLog.isDebugEnabled()) {
mLog.debug("Processing connector '" + aConnector.getId() + "' started...");
}
// notify plugins that a connector has started,
// i.e. a client was sconnected.
mPlugInChain.connectorStarted(aConnector);
}
super.connectorStarted(aConnector);
}
@Override
public void connectorStopped(WebSocketConnector aConnector, CloseReason aCloseReason) {
// notify plugins that a connector has stopped,
// i.e. a client was disconnected.
if (aConnector.getBool(VAR_IS_TOKENSERVER)) {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing connector '"
+ aConnector.getId() + "' stopped...");
}
mPlugInChain.connectorStopped(aConnector, aCloseReason);
}
super.connectorStopped(aConnector, aCloseReason);
}
public Token packetToToken(WebSocketConnector aConnector, WebSocketPacket aDataPacket) {
String lFormat = aConnector.getHeader().getFormat();
return TokenFactory.packetToToken(lFormat, aDataPacket);
}
public WebSocketPacket tokenToPacket(WebSocketConnector aConnector, Token aToken) {
String lFormat = aConnector.getHeader().getFormat();
return TokenFactory.tokenToPacket(lFormat, aToken);
}
public void processFilteredToken(WebSocketConnector aConnector, Token aToken) {
getPlugInChain().processToken(aConnector, aToken);
// forward the token to the listener chain
List<WebSocketServerListener> lListeners = getListeners();
WebSocketServerTokenEvent lEvent = new WebSocketServerTokenEvent(aConnector, this);
for (WebSocketServerListener lListener : lListeners) {
if (lListener != null && lListener instanceof WebSocketServerTokenListener) {
((WebSocketServerTokenListener) lListener).processToken(lEvent, aToken);
}
}
}
private void processToken(WebSocketConnector aConnector, Token aToken) {
// before forwarding the token to the plug-ins push it through filter
// chain
// TODO: Remove this temporary hack with final release 1.0
// this was required to ensure upward compatibility from 0.10 to 0.11
String lNS = aToken.getNS();
if (lNS != null && lNS.startsWith("org.jWebSocket")) {
aToken.setNS("org.jwebsocket" + lNS.substring(14));
}
FilterResponse lFilterResponse = getFilterChain().processTokenIn(aConnector, aToken);
// only forward the token to the plug-in chain
// if filter chain does not response "aborted"
if (!lFilterResponse.isRejected()) {
processFilteredToken(aConnector, aToken);
}
}
@Override
public void processPacket(WebSocketEngine aEngine, final WebSocketConnector aConnector, WebSocketPacket aDataPacket) {
// is the data packet supposed to be interpreted as token?
if (aConnector.getBool(VAR_IS_TOKENSERVER)) {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing packet as token...");
}
final Token lToken = packetToToken(aConnector, aDataPacket);
if (lToken != null) {
boolean lRunReqInOwnThread = "true".equals(lToken.getString("spawnThread"));
// TODO: create list of running threads and close all properly
// on shutdown
if (lRunReqInOwnThread) {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing threaded token '" + lToken.toString() + "' from '" + aConnector + "'...");
}
mCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
processToken(aConnector, lToken);
}
});
} else {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing token '" + lToken.toString() + "' from '" + aConnector + "'...");
}
processToken(aConnector, lToken);
}
} else {
mLog.error("Packet '" + aDataPacket.toString() + "' could not be converted into token.");
}
} else {
if (mLog.isDebugEnabled()) {
mLog.debug("Processing packet as custom packet...");
}
}
super.processPacket(aEngine, aConnector, aDataPacket);
}
public void sendToken(WebSocketConnector aSource, WebSocketConnector aTarget, Token aToken) {
sendTokenData(aSource, aTarget, aToken, false);
}
public void sendToken(WebSocketConnector aTarget, Token aToken) {
sendToken(null, aTarget, aToken);
}
public IOFuture sendTokenAsync(WebSocketConnector aTarget, Token aToken) {
return sendTokenData(null, aTarget, aToken, true);
}
public IOFuture sendTokenAsync(WebSocketConnector aSource, WebSocketConnector aTarget, Token aToken) {
return sendTokenData(aSource, aTarget, aToken, true);
}
public void sendToken(String aEngineId, String aConnectorId, Token aToken) {
// TODO: return meaningful result here.
WebSocketConnector lTargetConnector = getConnector(aEngineId, aConnectorId);
if (lTargetConnector != null) {
if (lTargetConnector.getBool(VAR_IS_TOKENSERVER)) {
// before sending the token push it through filter chain
FilterResponse lFilterResponse = getFilterChain().processTokenOut(null, lTargetConnector, aToken);
if (mLog.isDebugEnabled()) {
mLog.debug("Sending token '" + aToken + "' to '" + lTargetConnector + "'...");
}
sendPacketData(lTargetConnector, tokenToPacket(lTargetConnector, aToken), false);
} else {
mLog.warn("Connector not supposed to handle tokens.");
}
} else {
mLog.warn("Target connector '" + aConnectorId + "' not found.");
}
}
private IOFuture sendTokenData(WebSocketConnector aSource, WebSocketConnector aTarget, Token aToken, boolean aIsAsync) {
if (aTarget.getBool(VAR_IS_TOKENSERVER)) {
// before sending the token push it through filter chain
FilterResponse lFilterResponse = getFilterChain().processTokenOut(aSource, aTarget, aToken);
// only forward the token to the plug-in chain
// if filter chain does not response "aborted"
if (!lFilterResponse.isRejected()) {
if (mLog.isDebugEnabled()) {
mLog.debug("Sending token '" + aToken + "' to '" + aTarget + "'...");
}
WebSocketPacket lPacket = tokenToPacket(aTarget, aToken);
return sendPacketData(aTarget, lPacket, false);
} else {
if (mLog.isDebugEnabled()) {
mLog.debug("");
}
}
} else {
mLog.warn("Connector not supposed to handle tokens.");
}
return null;
}
private IOFuture sendPacketData(WebSocketConnector aTarget, WebSocketPacket aDataPacket, boolean aIsAsync) {
if (aIsAsync) {
return super.sendPacketAsync(aTarget, aDataPacket);
} else {
super.sendPacket(aTarget, aDataPacket);
return null;
}
}
/**
* Broadcasts the passed token to all token based connectors of the
* underlying engines that belong to the specified group.
*
* @param aToken
* - token to broadcast
*/
public void broadcastGroup(Token aToken) {
String lGroup = aToken.getString("group");
// if the group is not specified in the token then noone gets the
// message:
if (lGroup == null || lGroup.length() <= 0) {
mLog.debug("Token '" + aToken + "' has no group specified...");
return;
}
broadcastFiltered(aToken, "group", lGroup);
}
/**
* Broadcasts the passed token to all token based connectors of the
* underlying engines that belong to the specified filter and its name.
*
* @param aToken
* @param aFilterID
* @param aFilterName
*/
public void broadcastFiltered(Token aToken, String aFilterID, String aFilterName) {
if (mLog.isDebugEnabled()) {
mLog.debug("Broadcasting token '" + aToken + "' to all token based "
+ "connectors that belong to the filter '" + aFilterID + "' called '" + aFilterName + "'...");
}
FastMap<String, Object> lFilter = new FastMap<String, Object>();
lFilter.put(aFilterID, aFilterName);
broadcastFiltered(aToken, lFilter);
}
/**
* Broadcasts the passed token to all token based connectors of the
* underlying engines that belong to the specified filters.
*
* @param aToken
* @param aFilter
*/
public void broadcastFiltered(Token aToken, FastMap<String, Object> aFilter) {
if (mLog.isDebugEnabled()) {
mLog.debug("Broadcasting token '" + aToken + "' to all token based "
+ "connectors that belong to the filters...");
}
// before sending the token push it through filter chain
FilterResponse lFilterResponse = getFilterChain().processTokenOut(null, null, aToken);
aFilter.put(VAR_IS_TOKENSERVER, true);
// converting the token within the loop is removed in this method!
WebSocketPacket lPacket;
// lPackets maps protocol formats to appropriate converted packets:
FastMap<String, WebSocketPacket> lPackets = new FastMap<String, WebSocketPacket>();
String lFormat;
for (WebSocketConnector lConnector : selectConnectors(aFilter).values()) {
lFormat = lConnector.getHeader().getFormat();
lPacket = lPackets.get(lFormat);
// if there is no packet for this protocol format already, make one and
// store it in the map
if (lPacket == null) {
lPacket = tokenToPacket(lConnector, aToken);
lPackets.put(lFormat, lPacket);
}
sendPacket(lConnector, lPacket);
}
}
/**
* Broadcasts the passed token to all token based connectors of the
* underlying engines.
*
* @param aToken
*/
public void broadcastToken(Token aToken) {
if (mLog.isDebugEnabled()) {
mLog.debug("Broadcasting token '" + aToken + " to all token based connectors...");
}
// before sending the token push it through filter chain
FilterResponse lFilterResponse = getFilterChain().processTokenOut(null, null, aToken);
FastMap<String, Object> lFilter = new FastMap<String, Object>();
lFilter.put(VAR_IS_TOKENSERVER, true);
// converting the token within the loop is removed in this method!
WebSocketPacket lPacket;
// lPackets maps protocol formats to appropriate converted packets:
FastMap<String, WebSocketPacket> lPackets = new FastMap<String, WebSocketPacket>();
String lFormat;
for (WebSocketConnector lConnector : selectConnectors(lFilter).values()) {
lFormat = lConnector.getHeader().getFormat();
lPacket = lPackets.get(lFormat);
// if there is no packet for this protocol format already, make one and
// store it in the map
if (lPacket == null) {
lPacket = tokenToPacket(lConnector, aToken);
lPackets.put(lFormat, lPacket);
}
sendPacket(lConnector, lPacket);
}
}
/**
* Broadcasts to all connector, except the sender (aSource).
*
* @param aSource
* @param aToken
*/
public void broadcastToken(WebSocketConnector aSource, Token aToken) {
if (mLog.isDebugEnabled()) {
mLog.debug("Broadcasting token '" + aToken + " to all token based connectors...");
}
// before sending the token push it through filter chain
FilterResponse lFilterResponse = getFilterChain().processTokenOut(aSource, null, aToken);
Map<String, Object> lFilter = new FastMap<String, Object>();
lFilter.put(VAR_IS_TOKENSERVER, true);
// converting the token within the loop is removed in this method!
WebSocketPacket lPacket;
// lPackets maps protocol formats to appropriate converted packets:
Map<String, WebSocketPacket> lPackets = new FastMap<String, WebSocketPacket>();
String lFormat;
for (WebSocketConnector lConnector : selectConnectors(lFilter).values()) {
if (!aSource.equals(lConnector)) {
lFormat = lConnector.getHeader().getFormat();
lPacket = lPackets.get(lFormat);
// if there is no packet for this protocol format already, make one and
// store it in the map
if (lPacket == null) {
lPacket = tokenToPacket(lConnector, aToken);
lPackets.put(lFormat, lPacket);
}
sendPacket(lConnector, lPacket);
}
}
}
/**
* iterates through all connectors of all engines and sends the token to
* each connector. The token format is considered for each connection
* individually so that the application can broadcast a token to all kinds
* of clients.
*
* @param aSource
* @param aToken
* @param aBroadcastOptions
*/
public void broadcastToken(WebSocketConnector aSource, Token aToken, BroadcastOptions aBroadcastOptions) {
if (mLog.isDebugEnabled()) {
mLog.debug("Broadcasting token '" + aToken + " to all token based connectors...");
}
// before sending the token push it through filter chain
FilterResponse lFilterResponse = getFilterChain().processTokenOut(aSource, null, aToken);
Map<String, Object> lFilter = new FastMap<String, Object>();
lFilter.put(VAR_IS_TOKENSERVER, true);
// converting the token within the loop is removed in this method!
WebSocketPacket lPacket;
// lPackets maps protocol formats to appropriate converted packets:
Map<String, WebSocketPacket> lPackets = new FastMap<String, WebSocketPacket>();
String lFormat;
for (WebSocketConnector lConnector : selectConnectors(lFilter).values()) {
if (!aSource.equals(lConnector) || aBroadcastOptions.isSenderIncluded()) {
lFormat = lConnector.getHeader().getFormat();
lPacket = lPackets.get(lFormat);
// if there is no packet for this protocol format already, make one and
// store it in the map
if (lPacket == null) {
lPacket = tokenToPacket(lConnector, aToken);
lPackets.put(lFormat, lPacket);
}
sendPacket(lConnector, lPacket);
}
}
}
/**
* creates a standard response
*
* @param aInToken
* @return
*/
public Token createResponse(Token aInToken) {
Integer lTokenId = aInToken.getInteger("utid", -1);
String lType = aInToken.getString("type");
String lNS = aInToken.getString("ns");
Token lResToken = TokenFactory.createToken("response");
lResToken.setInteger("code", 0);
lResToken.setString("msg", "ok");
if (lTokenId != null) {
lResToken.setInteger("utid", lTokenId);
}
if (lNS != null) {
lResToken.setString("ns", lNS);
}
if (lType != null) {
lResToken.setString("reqType", lType);
}
return lResToken;
}
/**
* creates a response with the standard "not authenticated" message
*
* @param aInToken
* @return
*/
public Token createNotAuthToken(Token aInToken) {
Token lResToken = createResponse(aInToken);
lResToken.setInteger("code", -1);
lResToken.setString("msg", "not authenticated");
return lResToken;
}
/**
* creates a response with the standard "not granted" message
*
* @param aInToken
* @return
*/
public Token createAccessDenied(Token aInToken) {
Token lResToken = createResponse(aInToken);
lResToken.setInteger("code", -1);
lResToken.setString("msg", "access denied");
return lResToken;
}
/**
* @return the mPlugInChain
*/
@Override
public TokenPlugInChain getPlugInChain() {
return (TokenPlugInChain) mPlugInChain;
}
/**
* @return the mFilterChain
*/
@Override
public TokenFilterChain getFilterChain() {
return (TokenFilterChain) mFilterChain;
}
}