/*
* Copyright (c) 2005 Your Corporation. All Rights Reserved.
*/
package org.activemq.transport.stomp;
import EDU.oswego.cs.dl.util.concurrent.Channel;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArrayList;
import EDU.oswego.cs.dl.util.concurrent.LinkedQueue;
import org.activemq.io.WireFormat;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQTextMessage;
import org.activemq.message.ConnectionInfo;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.Packet;
import org.activemq.message.Receipt;
import org.activemq.message.SessionInfo;
import org.activemq.util.IdGenerator;
import javax.jms.JMSException;
import javax.jms.Session;
import java.io.BufferedReader;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.ProtocolException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Implements the TTMP protocol.
*/
public class StompWireFormat implements WireFormat
{
static final IdGenerator PACKET_IDS = new IdGenerator();
static final IdGenerator clientIds = new IdGenerator();
private CommandParser commandParser = new CommandParser(this);
private HeaderParser headerParser = new HeaderParser();
private DataInputStream in;
private String clientId;
private Channel pendingReadPackets = new LinkedQueue();
private Channel pendingWriteFrames = new LinkedQueue();
private List receiptListeners = new CopyOnWriteArrayList();
private String transactionId;
private short sessionId;
private Map subscriptions = new ConcurrentHashMap();
void addReceiptListener(ReceiptListener listener)
{
receiptListeners.add(listener);
}
public Packet readPacket(DataInput in) throws IOException
{
Packet pending = (Packet) AsyncHelper.tryUntilNotInterrupted(new AsyncHelper.HelperWithReturn()
{
public Object cycle() throws InterruptedException
{
return pendingReadPackets.poll(0);
}
});
if (pending != null)
{
return pending;
}
try
{
return commandParser.parse(in);
}
catch (ProtocolException e)
{
sendError(e.getMessage());
return null;
}
}
public Packet writePacket(final Packet packet, final DataOutput out) throws IOException, JMSException
{
flushPendingFrames(out);
if (packet.getPacketType() == Packet.RECEIPT_INFO)
{
assert(packet instanceof Receipt);
Receipt receipt = (Receipt) packet;
for (int i = 0; i < receiptListeners.size(); i++)
{
ReceiptListener listener = (ReceiptListener) receiptListeners.get(i);
if (listener.onReceipt(receipt, out))
{
receiptListeners.remove(listener);
return null;
}
}
}
if (packet.getPacketType() == Packet.ACTIVEMQ_TEXT_MESSAGE)
{
assert(packet instanceof ActiveMQTextMessage);
ActiveMQTextMessage msg = (ActiveMQTextMessage) packet;
Subscription sub = (Subscription) subscriptions.get(msg.getJMSDestination());
sub.receive(msg, out);
}
return null;
}
private void flushPendingFrames(final DataOutput out) throws IOException
{
boolean interrupted = false;
do
{
try
{
String frame = (String) pendingWriteFrames.poll(0);
if (frame == null) return;
out.writeBytes(frame);
}
catch (InterruptedException e)
{
interrupted = true;
}
}
while (interrupted);
}
private void sendError(final String message)
{
AsyncHelper.tryUntilNotInterrupted(new AsyncHelper.Helper()
{
public void cycle() throws InterruptedException
{
pendingWriteFrames.put(new FrameBuilder(Stomp.Responses.ERROR)
.addHeader(Stomp.Headers.Error.MESSAGE, message)
.toFrame());
}
});
}
/**
* some transports may register their streams (e.g. Tcp)
*
* @param dataOut
* @param dataIn
*/
public void registerTransportStreams(DataOutputStream dataOut, DataInputStream dataIn)
{
this.in = dataIn;
}
/**
* Some wire formats require a handshake at start-up
*
* @throws java.io.IOException
*/
public void initiateServerSideProtocol() throws IOException
{
BufferedReader in = new BufferedReader(new InputStreamReader(this.in));
String first_line = in.readLine();
if (!first_line.startsWith(Stomp.Commands.CONNECT))
{
throw new IOException("First line does not begin with with " + Stomp.Commands.CONNECT);
}
Properties headers = headerParser.parse(in);
//if (!headers.containsKey(TTMP.Headers.Connect.LOGIN))
// System.err.println("Required header [" + TTMP.Headers.Connect.LOGIN + "] missing");
//if (!headers.containsKey(TTMP.Headers.Connect.PASSCODE))
// System.err.println("Required header [" + TTMP.Headers.Connect.PASSCODE + "] missing");
// allow anyone to login for now
String login = headers.getProperty(Stomp.Headers.Connect.LOGIN);
String passcode = headers.getProperty(Stomp.Headers.Connect.PASSCODE);
// skip to end of the packet
while (in.read() != 0) {}
final ConnectionInfo info = new ConnectionInfo();
final Short packet_id = new Short(PACKET_IDS.getNextShortSequence());
clientId = clientIds.generateId();
commandParser.setClientId(clientId);
info.setClientId(clientId);
info.setReceiptRequired(true);
info.setClientVersion(Integer.toString(getCurrentWireFormatVersion()));
info.setClosed(false);
info.setHostName("ttmp.fake.host.name");
info.setId(packet_id.shortValue());
info.setUserName(login);
info.setPassword(passcode);
info.setStartTime(System.currentTimeMillis());
info.setStarted(true);
AsyncHelper.tryUntilNotInterrupted(new AsyncHelper.Helper()
{
public void cycle() throws InterruptedException
{
pendingReadPackets.put(info);
}
});
addReceiptListener(new ReceiptListener()
{
public boolean onReceipt(Receipt receipt, DataOutput out)
{
if (receipt.getCorrelationId() != packet_id.shortValue()) return false;
final Short session_packet_id = new Short(PACKET_IDS.getNextShortSequence());
sessionId = clientIds.getNextShortSequence();
commandParser.setSessionId(sessionId);
final SessionInfo info = new SessionInfo();
info.setStartTime(System.currentTimeMillis());
info.setId(session_packet_id.shortValue());
info.setClientId(clientId);
info.setSessionId(sessionId);
info.setStarted(true);
info.setSessionMode(Session.AUTO_ACKNOWLEDGE);
info.setReceiptRequired(true);
AsyncHelper.tryUntilNotInterrupted(new AsyncHelper.Helper()
{
public void cycle() throws InterruptedException
{
pendingReadPackets.put(info);
}
});
addReceiptListener(new ReceiptListener()
{
public boolean onReceipt(Receipt receipt, DataOutput out) throws IOException
{
if (receipt.getCorrelationId() != session_packet_id.shortValue()) return false;
StringBuffer buffer = new StringBuffer();
buffer.append(Stomp.Responses.CONNECTED).append(Stomp.NEWLINE);
buffer.append(Stomp.Headers.Connected.SESSION)
.append(Stomp.Headers.SEPERATOR)
.append(clientId)
.append(Stomp.NEWLINE)
.append(Stomp.NEWLINE);
buffer.append(Stomp.NULL);
out.writeBytes(buffer.toString());
return true;
}
});
return true;
}
});
}
/**
* Creates a new copy of this wire format so it can be used in another thread/context
*/
public WireFormat copy()
{
return new StompWireFormat();
}
/* Stuff below here is leaky stuff we don't actually need */
/**
* Some wire formats require a handshake at start-up
*
* @throws java.io.IOException
*/
public void initiateClientSideProtocol() throws IOException
{
throw new UnsupportedOperationException("Not yet implemented!");
}
/**
* Can this wireformat process packets of this version
*
* @param version the version number to test
* @return true if can accept the version
*/
public boolean canProcessWireFormatVersion(int version)
{
return version == getCurrentWireFormatVersion();
}
/**
* @return the current version of this wire format
*/
public int getCurrentWireFormatVersion()
{
return 1;
}
/**
* @return Returns the enableCaching.
*/
public boolean isCachingEnabled()
{
return false;
}
/**
* @param enableCaching The enableCaching to set.
*/
public void setCachingEnabled(boolean enableCaching)
{
// never
}
/**
* some wire formats will implement their own fragementation
*
* @return true unless a wire format supports it's own fragmentation
*/
public boolean doesSupportMessageFragmentation()
{
return false;
}
/**
* Some wire formats will not be able to understand compressed messages
*
* @return true unless a wire format cannot understand compression
*/
public boolean doesSupportMessageCompression()
{
return false;
}
/**
* Writes the given package to a new datagram
*
* @param channelID is the unique channel ID
* @param packet is the packet to write
* @return
* @throws java.io.IOException
* @throws javax.jms.JMSException
*/
public DatagramPacket writePacket(String channelID, Packet packet) throws IOException, JMSException
{
throw new UnsupportedOperationException("Will not be implemented");
}
/**
* Reads the packet from the given byte[]
*
* @param bytes
* @param offset
* @param length
* @return
* @throws java.io.IOException
*/
public Packet fromBytes(byte[] bytes, int offset, int length) throws IOException
{
throw new UnsupportedOperationException("Will not be implemented");
}
/**
* Reads the packet from the given byte[]
*
* @param bytes
* @return
* @throws java.io.IOException
*/
public Packet fromBytes(byte[] bytes) throws IOException
{
throw new UnsupportedOperationException("Will not be implemented");
}
/**
* A helper method which converts a packet into a byte array
*
* @param packet
* @return a byte array representing the packet using some wire protocol
* @throws java.io.IOException
* @throws javax.jms.JMSException
*/
public byte[] toBytes(Packet packet) throws IOException, JMSException
{
throw new UnsupportedOperationException("Will not be implemented");
}
/**
* A helper method for working with sockets where the first byte is read
* first, then the rest of the message is read.
* <p/>
* Its common when dealing with sockets to have different timeout semantics
* until the first non-zero byte is read of a message, after which
* time a zero timeout is used.
*
* @param firstByte the first byte of the packet
* @param in the rest of the packet
* @return
* @throws java.io.IOException
*/
public Packet readPacket(int firstByte, DataInput in) throws IOException
{
throw new UnsupportedOperationException("Will not be implemented");
}
/**
* Read a packet from a Datagram packet from the given channelID. If the
* packet is from the same channel ID as it was sent then we have a
* loop-back so discard the packet
*
* @param channelID is the unique channel ID
* @param dpacket
* @return the packet read from the datagram or null if it should be
* discarded
* @throws java.io.IOException
*/
public Packet readPacket(String channelID, DatagramPacket dpacket) throws IOException
{
throw new UnsupportedOperationException("Will not be implemented");
}
boolean isInTransaction()
{
return transactionId != null;
}
void setTransactionId(String transactionId)
{
this.transactionId = transactionId;
}
String getTransactionId()
{
return transactionId;
}
void clearTransactionId()
{
this.transactionId = null;
}
String getClientId()
{
return this.clientId;
}
public short getSessionId()
{
return sessionId;
}
public void addSubscription(Subscription s)
{
if (subscriptions.containsKey(s.getDestination()))
{
Subscription old = (Subscription) subscriptions.get(s.getDestination());
ConsumerInfo p = old.close();
enqueuePacket(p);
subscriptions.put(s.getDestination(), s);
}
else
{
subscriptions.put(s.getDestination(), s);
}
}
public void enqueuePacket(final Packet ack)
{
AsyncHelper.tryUntilNotInterrupted(new AsyncHelper.Helper()
{
public void cycle() throws InterruptedException
{
pendingReadPackets.put(ack);
}
});
}
public Subscription getSubscriptionFor(ActiveMQDestination destination)
{
return (Subscription) subscriptions.get(destination);
}
}