/**
* http://www.germane-software.com/software/Java/Gozirra/
*/
package org.chililog.client.stomp;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* Implements a Stomp server. This is a tiny embeddable server that can be used as an inter-net or intra-VM server to
* handle Stomp requests.
*
* For intra-VM requests, use getClient() to get an instance of a Stomp client.
*
* Example usage:
*
* <pre>
* Server s = new Server(61656); // To start it
* s.listen(12345); // To listen on another port
* // Note that 's' is now listening on TWO ports
* s.close(61656); // To close a port
* s.close(12345); // Now no ports are open.
* // This is effectively the same as:
* i = new Server(); // A server with no network (intra-VM)
* Stomp c = s.getClient(); // Creates and returns a client
* // that is connected to the server directly via VM method calls --
* // there is no network communication between this client and the
* // server.
* s.stop(); // To stop the server
* i.stop();
* </pre>
*
* FIXME Queues are not implemented. Therefore, this server operates as an IRC, rather than a Jabber, messaging system.
* That is, all messages arriving before a subscription request are lost to that client. When Queues are implemented,
* there will be an option to set persistence on the messages.
*
* Would it be good if -- given a session ID -- clients could reconnect and complete transactions?
*
* (c)2005 Sean Russell
*/
@SuppressWarnings({ "rawtypes", "unchecked", "unused" })
public class Server {
private Queue _message_queue;
private Map _transactions;
private Map _listeners;
private ConnectionListener _connection_listener;
private Authenticator _authenticator = new AllowAllAuthenticator();
/**
* Instantiates an intra-VM server. This will open no ports, and can not be connected to except by intra-VM threads.
* A port can be opened on this server by using the listen() method.
*
* @see listen()
*/
public Server() {
_message_queue = new FileQueue();
_transactions = new HashMap();
_listeners = new HashMap();
}
/**
* Instantiates an inter-network server listening on the supplied port. Additional ports can be listened on by using
* the listen() method.
*
* @see listen()
* @param port
* This port will be opened and will listen for client connections. If the port value is less than 0, the
* default port of 61626 will be used.
*/
public Server(int port) throws IOException {
this(port, null);
}
/**
* Instantiates an inter-network server listening on the supplied port. Additional ports can be listened on by using
* the listen() method.
*
* @see listen()
* @param port
* This port will be opened and will listen for client connections. If the port value is less than 0, the
* default port of 61626 will be used.
* @param auth
* A class responsible for authenticating connections.
*/
public Server(int port, Authenticator auth) throws IOException {
this();
if (port < 0)
port = 61626;
if (auth != null)
_authenticator = auth;
listen(port);
}
/**
* Opens a port for internet connections. A single server can listen on multiple ports, so calling this method
* multiple times will open multiple ports.
*
* @param port
* This port will be opened and will listen for client connections. If the port value is less than 0, an
* exception is thrown.
*/
public void listen(int port) throws IOException {
_connection_listener = new ConnectionListener(port, this);
_connection_listener.start();
}
/**
* Called by a SocketHandler to notify the server that a client has disconnected. Is not, and should not, be called
* from anywhere else.
*/
protected void disconnect(SocketHandler s) {
_connection_listener.disconnect(s);
}
/**
* This class is necessary because Java is RETARDED. Specifically, it lacks closures.
*
* Listenens on a port and accepts client connections. For each connection, spawns a SocketHandler for the
* connection. When shut down, stops receiving connections and shuts down all existing client connections.
*/
private class ConnectionListener extends Thread {
private int _port;
private Server _server;
private ServerSocket _serve_sock;
private List _handlers = new ArrayList();
protected ConnectionListener(int port, Server server) {
_port = port;
_server = server;
}
public void run() {
Socket sock = null;
try {
_serve_sock = new ServerSocket(_port);
while (!isInterrupted()) {
sock = _serve_sock.accept();
try {
Thread handler = new SocketHandler(sock, _server);
handler.start();
_handlers.add(handler);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}
} catch (SocketException e) {
// This gets thrown when the accept() is interrupted
} catch (IOException e) {
e.printStackTrace(System.err);
} catch (Exception e) {
e.printStackTrace(System.err);
}
for (Iterator i = _handlers.iterator(); i.hasNext();) {
try {
Thread t = (Thread) i.next();
t.interrupt();
Thread.yield();
} catch (Exception e) {
}
}
}
/**
* Shut down operations.
*/
protected void shutdown() {
this.interrupt();
try {
_serve_sock.close();
} catch (Exception e) {
}
}
/**
* Called by the server to notify this object that a SocketHandler has disconnected itself.
*/
protected void disconnect(SocketHandler h) {
_handlers.remove(h);
}
}
/**
* Shuts down the server, closing all connections.
*/
public void stop() {
// The _connection_listener will be null if this is not a network
// socket server.
if (_connection_listener != null) {
_connection_listener.shutdown();
}
close(-1);
Thread.yield();
}
/**
* Sets the queuing mechanism used for all further messages. Any existing undelivered messages will <em>not</em> use
* this queue. FIXME Not implemented
*
* @param queue
*/
public void setQueue(Queue q) {
_message_queue = q;
}
/**
* Closes a port. All connections on this port will be closed.
*
* @param port
* The port to close. A value of < -1 closes all ports
*/
public void close(int port) {
for (Iterator i = _listeners.keySet().iterator(); i.hasNext();) {
Object k = i.next();
Object s = _listeners.get(k);
if (s instanceof SocketHandler) {
SocketHandler sh = (SocketHandler) s;
if (port == -1 || sh.isPort(port)) {
sh.interrupt();
sh.close();
_transactions.remove(s);
_listeners.remove(k);
}
}
}
}
// FIXME: Need to enforce CONNECT; right now, doesn't require a connect.
// FIXME: Add login handling feature
/**
* Manages client connections. There is one SocketHandler per client. This class is responsible for relaying
* communications between the server and the client for which it is responsible.
*/
protected class SocketHandler extends Receiver implements Listener, Authenticatable {
private InputStream _input;
private OutputStream _output;
private Socket _socket;
private Server _server;
private Object _client_token;
private boolean _authenticated = false;
/**
* Sets up a client communication on a given socket.
*/
public SocketHandler(Socket sock, Server s) throws IOException {
super();
_input = sock.getInputStream();
_output = sock.getOutputStream();
_socket = sock;
_server = s;
setup(this, _input);
}
public Object token() {
return _client_token;
}
public boolean isClosed() {
return _socket.isClosed();
}
/**
* Tests whether the supplied port is the port this handler is communicating with the client over.
*
* @param port
* the port number to test.
* @return true iff the supplied port matches the open port of this handler.
*/
protected boolean isPort(int port) {
return _socket.getPort() == port;
}
/**
* Close the connection with the client.
*/
protected void close() {
try {
_socket.shutdownInput();
_input.close();
} catch (IOException e) { /* Who cares? */
}
try {
_socket.shutdownOutput();
_output.close();
} catch (IOException e) { /* Who cares? */
}
try {
_socket.close();
} catch (IOException e) { /* Who cares? */
}
}
public void disconnect() {
close();
}
/**
* Gets called when messages come in from the client, and relays the message to the server. This method handles
* and consumes CONNECT, DISCONNECT, and ERROR messages. It is also responsible for sending RECEIPTs back to the
* client.
*/
public void receive(Command c, Map h, String b) {
if (c == Command.CONNECT) {
String login = (String) h.get("login");
String passcode = (String) h.get("passcode");
try {
_client_token = Server.this._authenticator.connect(login, passcode);
HashMap headers = new HashMap();
headers.put("session", String.valueOf(this.hashCode()));
transmit(Command.CONNECTED, headers, null);
_authenticated = true;
} catch (javax.security.auth.login.LoginException e) {
transmit(Command.ERROR, null, "Login failed: " + e.getMessage());
}
} else {
if (!_authenticated) {
transmit(Command.ERROR, null, "Not CONNECTed, or not authorized");
return;
}
if (c == Command.DISCONNECT) {
if (h != null) {
String receipt = (String) h.get("receipt");
if (receipt != null) {
HashMap headers = new HashMap();
headers.put("receipt-id", receipt);
receive(Command.RECEIPT, headers, null);
}
}
_server.disconnect(this);
this.interrupt();
Thread.yield();
close();
} else if (c == Command.ERROR) {
// Then there was an error in the client message. Pass it back.
error(h, b);
} else {
_server.receive(c, h, b, this);
}
}
}
/**
* Called by the server; sends a message to this client.
*/
public void message(Map headers, String body) {
transmit(Command.MESSAGE, headers, body);
}
/**
* Called by the server; sends a receipt to this client.
*/
public void receipt(Map headers) {
transmit(Command.RECEIPT, headers, null);
}
/**
* Called by the server. Sends an error to the client.
*/
public void error(Map headers, String message) {
transmit(Command.ERROR, headers, message);
}
/**
* Used by message(), receipt(), and error() to deliver the message to the client.
*/
private void transmit(Command c, Map h, String b) {
try {
Transmitter.transmit(c, h, b, _output);
} catch (Exception e) {
this.interrupt();
Thread.yield();
close();
}
}
}
private String mapToStr(Map m) {
StringBuffer b = new StringBuffer("[ ");
for (Iterator keys = m.keySet().iterator(); keys.hasNext();) {
String k = keys.next().toString();
b.append(k + " => " + m.get(k) + ", ");
}
b.append("]");
return b.toString();
}
/**
* Incoming mesages from clients come here, and are delivered to listeners, both intra-VM and network.
*
* @param c
* the command
* @param h
* the headers
* @param b
* the message
* @param y
* the thing that received the message and passed it to us
*/
protected void receive(Command c, Map h, String b, Authenticatable y) {
long id = (int) (Math.random() * 10000);
try {
// Convert to MESSAGE and distribute
if (c == Command.COMMIT) {
synchronized (_transactions) {
List trans = (List) _transactions.remove(y);
trans = new ArrayList(trans);
for (Iterator i = trans.iterator(); i.hasNext();) {
Message m = (Message) i.next();
try {
receive(m.command(), m.headers(), m.body(), y);
} catch (Exception e) {
// Don't allow listener code to break us
}
}
}
} else if (c == Command.ABORT) {
synchronized (_transactions) {
_transactions.remove(y);
}
} else if (_transactions.get(y) != null) {
synchronized (_transactions) {
((List) _transactions.get(y)).add(new Message(c, h, b));
}
} else {
if (h == null)
h = new HashMap();
String destination = (String) h.get("destination");
if (c == Command.SEND) {
if (y instanceof IntraVMClient || _authenticator.authorizeSend(y.token(), destination)) {
synchronized (_listeners) {
List l = (List) _listeners.get(destination);
if (l != null) {
l = new ArrayList(l);
for (Iterator i = l.iterator(); i.hasNext();) {
Listener sh = (Listener) i.next();
try {
sh.message(h, b);
} catch (Exception e) {
// Don't allow listener code to break us
}
}
}
}
} else {
Map error_headers = new HashMap();
error_headers.put("message:", "authorization refused");
error_headers.put("type:", "send");
error_headers.put("channel:", destination);
y.error(error_headers, "The message:\n-----\n" + b
+ "\n-----\nAuthentication token refused for this channel");
}
} else if (c == Command.SUBSCRIBE) {
if (y instanceof IntraVMClient || _authenticator.authorizeSubscribe(y.token(), destination)) {
synchronized (_listeners) {
List l = (List) _listeners.get(destination);
if (l == null) {
l = new ArrayList();
_listeners.put(destination, l);
}
if (!l.contains(y))
l.add(y);
}
} else {
Map error_headers = new HashMap();
error_headers.put("message:", "authorization refused");
error_headers.put("type:", "subscription");
error_headers.put("channel:", destination);
y.error(error_headers, "The message:\n-----\n" + b
+ "\n-----\nAuthentication token refused for this channel");
}
} else if (c == Command.UNSUBSCRIBE) {
synchronized (_listeners) {
List l = (List) _listeners.get(destination);
if (l != null)
l.remove(y);
}
} else if (c == Command.BEGIN) {
synchronized (_transactions) {
List trans = new ArrayList();
_transactions.put(y, trans);
}
} else if (c == Command.DISCONNECT) {
synchronized (_listeners) {
for (Iterator i = _listeners.values().iterator(); i.hasNext();) {
List l = (List) i.next();
l.remove(y);
}
}
}
}
if (h != null) {
String receipt = (String) h.get("receipt");
if (receipt != null) {
HashMap headers = new HashMap();
headers.put("receipt-id", receipt);
y.receive(Command.RECEIPT, headers, null);
}
}
} catch (Exception e) {
// Don't allow listener code to break us
}
}
/**
* Returns a Stomp client for intra-VM communications with the server. This client communicates directly with the
* server via method() calls, bypassing the network.
*/
public Stomp getClient() {
return new IntraVMClient(this);
}
/**
* Gozirra is probably not the best choice for a stand-alone server. If you are tempted to use it as such, you might
* want to look at ActiveMQ, which is a feature-rich MOM solution. Gozirra is intended primarily to be an
* ultra-light embedded messaging library.
*
* If you still want to run Gozirra as a stand-alone server, then you'll need to know about how to call it. The
* main() method takes a single argument: a port to run on. It will run until you ^C it.
*/
public static final void main(String[] args) {
if (args.length != 1) {
System.err.println("A single argument -- a port -- is required");
System.exit(1);
}
int port = Integer.valueOf(args[0]).intValue();
System.out.println(Version.VERSION);
try {
new Server(port);
} catch (Exception e) {
System.err.println("Failed to start server");
e.printStackTrace(System.err);
}
}
}