/*
* MicroJIAC - A Lightweight Agent Framework
* This file is part of MicroJIAC STOMP-Client.
*
* Copyright (c) 2007-2012 DAI-Labor, Technische Universität Berlin
*
* This library includes software developed at DAI-Labor, Technische
* Universität Berlin (http://www.dai-labor.de)
*
* This library 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 library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* $Id: StompTransport.java 23214 2009-05-06 12:18:42Z marcel $
*/
package de.jiac.micro.ext.stomp;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import com.github.libxjava.concurrent.AbstractSingleThreadRunner;
import com.github.libxjava.io.ByteArrayOutputBuffer;
import com.github.libxjava.util.SerialisableHashtable;
import de.jiac.micro.core.handle.IResourceHandle;
import de.jiac.micro.core.io.IMessage;
import de.jiac.micro.core.io.IStreamConnection;
import de.jiac.micro.core.scope.AbstractScopeAwareRunner;
import de.jiac.micro.core.scope.Scope;
import de.jiac.micro.internal.io.Message;
/**
* Implementation of the STOMP protocol (client side) as a MicroJIAC transport.
*
* @author Marcel Patzlaff
* @version $Revision: 23214 $
*/
public final class StompTransport extends Transport {
public static final String GROUP_PREFIX= "G#";
public static final String MBOX_PREFIX= "M#";
public static final String PREFIX_HEADER= "stomp_prefix";
public static final String DESTINATION_HEADER= "stomp_destination";
/*package*/ static final byte[] TOPIC_BYTES;
/*package*/ static final byte[] QUEUE_BYTES;
private static final byte[] CMD_SUBSCRIBE;
private static final byte[] CMD_UNSUBSCRIBE;
private static final byte[] CMD_SEND;
private static final byte[] CMD_CONNECT;
private static final byte[] CMD_DISCONNECT;
/*package*/ static final byte[] NO_CONTENT;
/*package*/ static final String STOMP_DESTINATION= "destination";
private final static Vector IGNORE_HEADERS;
static {
IGNORE_HEADERS = new Vector();
IGNORE_HEADERS.addElement(STOMP_DESTINATION);
IGNORE_HEADERS.addElement("receipt");
IGNORE_HEADERS.addElement("content-length");
IGNORE_HEADERS.addElement(PREFIX_HEADER);
IGNORE_HEADERS.addElement(DESTINATION_HEADER);
TOPIC_BYTES = new byte[] {(byte)'/', (byte)'t', (byte)'o', (byte)'p', (byte)'i', (byte)'c', (byte)'/'};
QUEUE_BYTES = new byte[] {(byte)'/', (byte)'q', (byte)'u', (byte)'e', (byte)'u', (byte)'e', (byte)'/'};
CMD_SUBSCRIBE = new byte[] {(byte)'S', (byte)'U', (byte)'B', (byte)'S', (byte)'C', (byte)'R', (byte)'I', (byte)'B', (byte)'E'};
CMD_UNSUBSCRIBE = new byte[] {(byte)'U', (byte)'N', (byte)'S', (byte)'U', (byte)'B', (byte)'S', (byte)'C', (byte)'R', (byte)'I', (byte)'B', (byte)'E'};
CMD_SEND = new byte[] {(byte)'S', (byte)'E', (byte)'N', (byte)'D'};
CMD_CONNECT= new byte[] {(byte)'C', (byte)'O', (byte)'N', (byte)'N', (byte)'E', (byte)'C', (byte)'T'};
CMD_DISCONNECT= new byte[] {(byte)'D', (byte)'I', (byte)'S', (byte)'C', (byte)'O', (byte)'N', (byte)'N', (byte)'E', (byte)'C', (byte)'T'};
NO_CONTENT= new byte[0];
}
/*package*/ final class Receiver extends AbstractScopeAwareRunner {
private StringBuffer _firstBuffer= new StringBuffer();
private StringBuffer _secondBuffer= new StringBuffer();
private ByteArrayOutputBuffer _binaryBuffer= new ByteArrayOutputBuffer();
public Receiver() {
super("stomp-receiver");
}
protected void doRun() {
final IStreamConnection connection= _connection;
try {
while(!isCancelled() && connection != null && connection == _connection) {
internalReceive();
}
} catch (Exception e) {
if(connection == _connection) {
delegate.onError(StompTransport.this, e);
}
}
}
protected void internalReceive() throws IOException {
ensureOpen();
InputStream in= _connection.getInputStream();
synchronized(in) {
SerialisableHashtable headers= new SerialisableHashtable();
String command= null;
String destPrefix= null;
String destName= null;
byte[] content= NO_CONTENT;
int ch;
_firstBuffer.setLength(0);
_secondBuffer.setLength(0);
StringBuffer strBuffer= _firstBuffer;
boolean headersClosed= false;
boolean headersOpen= false;
// read header section -> null bytes are normal values here
for(ch= in.read();; ch= in.read()) {
if(ch < 0) {
throw new EOFException("unexpected end of stream");
}
switch (ch) {
case '\n': {
if(_firstBuffer.length() <= 0) {
if(headersOpen) {
headersClosed= true;
}
} else {
headersOpen= true;
String key= dropWhiteSpaces(_firstBuffer).toString();
if(_secondBuffer.length() <= 0 && command == null) {
command= key;
} else {
dropWhiteSpaces(_secondBuffer);
if(key.equals(STOMP_DESTINATION)) {
// convert STOMP destination to MicroJIAC destination
if(_secondBuffer.charAt(1) == 't') {
_secondBuffer.delete(0, TOPIC_BYTES.length);
destPrefix= GROUP_PREFIX;
} else {
_secondBuffer.delete(0, QUEUE_BYTES.length);
destPrefix= MBOX_PREFIX;
}
destName= _secondBuffer.toString().toLowerCase();
} else {
headers.put(key, _secondBuffer.toString());
}
}
_firstBuffer.setLength(0);
_secondBuffer.setLength(0);
}
strBuffer= _firstBuffer;
break;
}
case ':': {
if(strBuffer == _firstBuffer) {
strBuffer= _secondBuffer;
break;
}
// fall through if we are in second buffer
}
default: {
strBuffer.append((char)ch);
}
}
if(headersOpen && headersClosed) {
break;
}
}
String contentLength= (String) headers.get("content-length");
if(contentLength != null) {
try {
int toRead= Integer.parseInt(contentLength);
if(toRead < 0) {
throw new IOException("specified content-length '" + contentLength + "' is invalid");
} else if(toRead > 0) {
content= new byte[toRead];
for(int offset= 0; toRead > 0;) {
int numBytes= in.read(content, offset, toRead);
if(numBytes < 0) {
throw new EOFException("unexpected end-of-stream: needed to read '" + toRead + "' more bytes");
}
offset+= numBytes;
toRead-= numBytes;
}
}
// check and consume trailing null byte
if(in.read() != 0) {
throw new IOException("content-length bytes read but there was no trailing null byte");
}
} catch (NumberFormatException nfe) {
throw new IOException("specified content-length '" + contentLength + "' is no valid integer");
}
} else {
_binaryBuffer.reset();
// read content until null byte is consumed
while((ch= in.read()) != 0) {
_binaryBuffer.write(ch);
}
content= _binaryBuffer.toByteArray();
}
receive(command, destPrefix, destName, headers, content);
}
}
protected StringBuffer dropWhiteSpaces(StringBuffer buffer) {
char ch;
int offset;
// remove whitespaces at head
offset= -1;
for(int i= 0; i < buffer.length(); ++i) {
ch= buffer.charAt(i);
if(ch == ' ' || ch == '\r' || ch == '\n') {
offset= i;
} else {
break;
}
}
if(offset >= 0) {
buffer.delete(0, offset + 1);
}
// remove whitespace at tail
offset= -1;
for(int i= buffer.length() - 1; i >= 0; --i) {
ch= buffer.charAt(i);
if(ch == ' ' || ch == '\r' || ch == '\n') {
offset= i;
} else {
break;
}
}
if(offset >= 0) {
buffer.delete(offset, buffer.length());
}
return buffer;
}
}
private final Hashtable _loginData= new Hashtable();
private final Hashtable _subscribeData= new Hashtable();
private final Receiver _receiver= new Receiver();
private String _serverURL= null;
private ContentTransformer _transformer;
/*package*/ volatile IStreamConnection _connection = null;
public StompTransport() {
_subscribeData.put("activemq.noLocal", "true");
}
// === Methods from Transport === //
public void doRegister(String prefix, String name) throws IOException {
transmit(CMD_SUBSCRIBE, prefix, name, _subscribeData, null);
}
public void doSend(IMessage m) throws IOException {
String prefix= m.getHeader(PREFIX_HEADER);
String destination= m.getHeader(DESTINATION_HEADER);
Message message= (Message) m;
transmit(CMD_SEND, prefix, destination, message.getHeaders(), _transformer.toByteArray(message.getContent()));
}
public void doUnregister(String prefix, String name) throws IOException {
transmit(CMD_UNSUBSCRIBE, prefix, name, null, null);
}
public synchronized void doStart() throws IOException {
if(_serverURL == null) {
throw new IOException("server URL is not set");
}
if(_connection != null) {
doStop();
}
_transformer= new ContentTransformer(delegate.getClassLoader());
IResourceHandle rh= (IResourceHandle) Scope.getContainer().getHandle(IResourceHandle.class);
_connection= rh.openStreamConnection(_serverURL);
delegate.getLogger().debug("stomp: open connection to '" + _serverURL + "'");
// initialise the connection
transmit(CMD_CONNECT, null, null, _loginData, null);
try {
_receiver.start();
_receiver.waitForState(2000, AbstractSingleThreadRunner.STARTED);
} catch (InterruptedException ie) {
delegate.getLogger().error("stomp: could not start receiver", ie);
}
}
// === Initialisation Methods === //
public synchronized void doStop() {
if(_connection != null) {
delegate.getLogger().debug("stomp: closing connection to remote broker");
try {transmit(CMD_DISCONNECT, null, null, null, null);} catch (IOException ioe) {/* ignore it */}
final IStreamConnection c= _connection;
_connection= null;
c.close();
try {
_receiver.stop();
_receiver.waitForState(2000, AbstractSingleThreadRunner.STOPPED);
} catch (InterruptedException ie) {
delegate.getLogger().warn("stomp: could not stop receiver", ie);
}
_transformer= null;
}
}
/**
* The server URL must be set before {@link #doStart()} is called.
* It specifies the URL where the remote broker runs.
*
* @param url the URL of the remote broker
*/
public void setServerURL(String url) {
_serverURL= url;
}
public void setLogin(String login) {
if(login == null) {
_loginData.remove("login");
} else {
_loginData.put("login", login);
}
}
public void setPasscode(String passcode) {
if(passcode == null) {
_loginData.remove("passcode");
} else {
_loginData.put("passcode", passcode);
}
}
protected void receive(String command, String destPrefix, String destName, SerialisableHashtable headers, byte[] content) {
if("MESSAGE".equalsIgnoreCase(command)) {
try {
IMessage message= new Message(headers, _transformer.toObject(content));
delegate.onMessage(this, message, destName);
} catch (Exception e) {
delegate.onError(this, e);
}
}
}
/*package*/ void ensureOpen() throws IOException {
if (_connection == null) {
throw new IOException("connection is closed");
}
}
private void transmit(byte[] command, String destPrefix, String destName, Hashtable headers, byte[] content) throws IOException {
ensureOpen();
OutputStream out = _connection.getOutputStream();
synchronized (out) {
out.write(command);
out.write('\n');
if (destPrefix != null) {
out.write(STOMP_DESTINATION.getBytes());
out.write(':');
out.write(destPrefix == GROUP_PREFIX ? TOPIC_BYTES : QUEUE_BYTES);
out.write(destName.getBytes());
out.write('\n');
}
if (headers != null) {
String key;
String value;
for (Enumeration keys = headers.keys(); keys.hasMoreElements();) {
key = keys.nextElement().toString();
if (!IGNORE_HEADERS.contains(key)) {
value = headers.get(key).toString();
out.write(key.getBytes());
out.write(':');
out.write(value.getBytes());
out.write('\n');
}
}
}
if (content != null) {
out.write("content-length:".getBytes());
out.write(Integer.toString(content.length).getBytes());
out.write('\n');
out.write('\n');
out.write(content);
} else {
out.write('\n');
}
out.write('\u0000');
out.flush();
}
}
}