/*
* Copyright 2010 Red Hat, Inc.
* Red Hat licenses this file to you under the Apache License, version
* 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.hornetq.core.protocol.stomp;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import org.hornetq.api.core.HornetQBuffer;
import org.hornetq.api.core.HornetQException;
import org.hornetq.api.core.Interceptor;
import org.hornetq.api.core.Message;
import org.hornetq.api.core.SimpleString;
import org.hornetq.api.core.client.HornetQClient;
import org.hornetq.core.journal.IOAsyncTask;
import org.hornetq.core.logging.Logger;
import org.hornetq.core.postoffice.Bindings;
import org.hornetq.core.server.HornetQServer;
import org.hornetq.core.server.ServerSession;
import org.hornetq.core.server.impl.ServerMessageImpl;
import org.hornetq.spi.core.protocol.ConnectionEntry;
import org.hornetq.spi.core.protocol.ProtocolManager;
import org.hornetq.spi.core.protocol.RemotingConnection;
import org.hornetq.spi.core.remoting.Acceptor;
import org.hornetq.spi.core.remoting.Connection;
import org.hornetq.spi.core.security.HornetQSecurityManager;
import org.hornetq.utils.UUIDGenerator;
/**
* StompProtocolManager
*
* @author <a href="mailto:jmesnil@redhat.com">Jeff Mesnil</a>
*/
class StompProtocolManager implements ProtocolManager
{
// Constants -----------------------------------------------------
private static final SimpleString JMS_TOPIC_PREFIX = new SimpleString("jms.topic");
private static final SimpleString JMS_QUEUE_PREFIX = new SimpleString("jms.queue");
private static final Logger log = Logger.getLogger(StompProtocolManager.class);
// TODO use same value than HornetQConnection
private static final String CONNECTION_ID_PROP = "__HQ_CID";
// Attributes ----------------------------------------------------
private final HornetQServer server;
private final SimpleString managementAddress;
private final Executor executor;
private final Map<String, StompSession> transactedSessions = new HashMap<String, StompSession>();
// key => connection ID, value => Stomp session
private final Map<Object, StompSession> sessions = new HashMap<Object, StompSession>();
// Static --------------------------------------------------------
private static StompFrame createError(Exception e, StompFrame request)
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try
{
// Let the stomp client know about any protocol errors.
PrintWriter stream = new PrintWriter(new OutputStreamWriter(baos, "UTF-8"));
e.printStackTrace(stream);
stream.close();
Map<String, Object> headers = new HashMap<String, Object>();
headers.put(Stomp.Headers.Error.MESSAGE, e.getMessage());
final String receiptId = (String)request.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED);
if (receiptId != null)
{
headers.put(Stomp.Headers.Response.RECEIPT_ID, receiptId);
}
byte[] payload = baos.toByteArray();
headers.put(Stomp.Headers.CONTENT_LENGTH, payload.length);
return new StompFrame(Stomp.Responses.ERROR, headers, payload);
}
catch (UnsupportedEncodingException ex)
{
log.warn("Unable to create ERROR frame from the exception", ex);
return null;
}
}
// Constructors --------------------------------------------------
public StompProtocolManager(final HornetQServer server, final List<Interceptor> interceptors)
{
this.server = server;
this.managementAddress = server.getConfiguration().getManagementAddress();
this.executor = server.getExecutorFactory().getExecutor();
}
// ProtocolManager implementation --------------------------------
public ConnectionEntry createConnectionEntry(final Acceptor acceptorUsed, final Connection connection)
{
StompConnection conn = new StompConnection(acceptorUsed, connection, this, server.getExecutorFactory().getExecutor());
// Note that STOMP has no heartbeat, so if connection ttl is non zero, data must continue to be sent or connection
// will be timed out and closed!
long ttl = server.getConfiguration().getConnectionTTLOverride();
if (ttl != -1)
{
return new ConnectionEntry(conn, null, System.currentTimeMillis(), ttl);
}
else
{
// Default to 1 minute - which is same as core protocol
return new ConnectionEntry(conn, null, System.currentTimeMillis(), 1 * 60 * 1000);
}
}
public void removeHandler(String name)
{
}
public int isReadyToHandle(HornetQBuffer buffer)
{
// This never gets called
return -1;
}
public void handleBuffer(final RemotingConnection connection, final HornetQBuffer buffer)
{
long start = System.nanoTime();
StompConnection conn = (StompConnection)connection;
conn.setDataReceived();
StompDecoder decoder = conn.getDecoder();
// log.info("in handle");
do
{
StompFrame request;
try
{
request = decoder.decode(buffer);
}
catch (Exception e)
{
log.error("Failed to decode", e);
return;
}
if (request == null)
{
break;
}
try
{
String command = request.getCommand();
StompFrame response = null;
if (Stomp.Commands.CONNECT.equals(command))
{
response = onConnect(request, conn);
}
else if (Stomp.Commands.DISCONNECT.equals(command))
{
response = onDisconnect(request, conn);
}
else if (Stomp.Commands.SEND.equals(command))
{
response = onSend(request, conn);
}
else if (Stomp.Commands.SUBSCRIBE.equals(command))
{
response = onSubscribe(request, conn);
}
else if (Stomp.Commands.UNSUBSCRIBE.equals(command))
{
response = onUnsubscribe(request, conn);
}
else if (Stomp.Commands.ACK.equals(command))
{
response = onAck(request, conn);
}
else if (Stomp.Commands.BEGIN.equals(command))
{
response = onBegin(request, server, conn);
}
else if (Stomp.Commands.COMMIT.equals(command))
{
response = onCommit(request, conn);
}
else if (Stomp.Commands.ABORT.equals(command))
{
response = onAbort(request, conn);
}
else
{
log.error("Unsupported Stomp frame: " + request);
response = new StompFrame(Stomp.Responses.ERROR,
new HashMap<String, Object>(),
("Unsupported frame: " + command).getBytes());
}
if (request.getHeaders().containsKey(Stomp.Headers.RECEIPT_REQUESTED))
{
if (response == null)
{
Map<String, Object> h = new HashMap<String, Object>();
response = new StompFrame(Stomp.Responses.RECEIPT, h);
}
response.getHeaders().put(Stomp.Headers.Response.RECEIPT_ID,
request.getHeaders().get(Stomp.Headers.RECEIPT_REQUESTED));
}
if (response != null)
{
sendReply(conn, response);
}
if (Stomp.Commands.DISCONNECT.equals(command))
{
conn.destroy();
}
}
catch (Exception e)
{
e.printStackTrace();
StompFrame error = createError(e, request);
if (error != null)
{
sendReply(conn, error);
}
}
finally
{
server.getStorageManager().clearContext();
}
} while (decoder.hasBytes());
long end = System.nanoTime();
// log.info("handle took " + (end-start));
}
// Public --------------------------------------------------------
public void send(final StompConnection connection, final StompFrame frame)
{
if (log.isTraceEnabled())
{
log.trace("sent " + frame);
}
synchronized (connection)
{
if (connection.isDestroyed() || !connection.isValid())
{
log.warn("Connection closed " + connection);
return;
}
try
{
HornetQBuffer buffer = frame.toHornetQBuffer();
connection.getTransportConnection().write(buffer, false, false);
}
catch (Exception e)
{
log.error("Unable to send frame " + frame, e);
}
}
}
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
// Private -------------------------------------------------------
private StompFrame onSubscribe(StompFrame frame, StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String destination = (String)headers.get(Stomp.Headers.Subscribe.DESTINATION);
String selector = (String)headers.get(Stomp.Headers.Subscribe.SELECTOR);
String ack = (String)headers.get(Stomp.Headers.Subscribe.ACK_MODE);
String id = (String)headers.get(Stomp.Headers.Subscribe.ID);
String durableSubscriptionName = (String)headers.get(Stomp.Headers.Subscribe.DURABLE_SUBSCRIBER_NAME);
boolean noLocal = false;
if (headers.containsKey(Stomp.Headers.Subscribe.NO_LOCAL))
{
noLocal = Boolean.parseBoolean((String)headers.get(Stomp.Headers.Subscribe.NO_LOCAL));
}
if (noLocal)
{
String noLocalFilter = CONNECTION_ID_PROP + " <> '" + connection.getID().toString() + "'";
if (selector == null)
{
selector = noLocalFilter;
}
else
{
selector += " AND " + noLocalFilter;
}
}
if (ack == null)
{
ack = Stomp.Headers.Subscribe.AckModeValues.AUTO;
}
String subscriptionID = null;
if (id != null)
{
subscriptionID = id;
}
else
{
if (destination == null)
{
throw new StompException("Client must set destination or id header to a SUBSCRIBE command");
}
subscriptionID = "subscription/" + destination;
}
validateDestination(new SimpleString(destination));
StompSession stompSession = getSession(connection);
stompSession.setNoLocal(noLocal);
if (stompSession.containsSubscription(subscriptionID))
{
throw new StompException("There already is a subscription for: " + subscriptionID +
". Either use unique subscription IDs or do not create multiple subscriptions for the same destination");
}
long consumerID = server.getStorageManager().generateUniqueID();
String clientID = (connection.getClientID() != null) ? connection.getClientID() : null;
stompSession.addSubscription(consumerID,
subscriptionID,
clientID,
durableSubscriptionName,
destination,
selector,
ack);
return null;
}
private StompFrame onUnsubscribe(StompFrame frame, StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String destination = (String)headers.get(Stomp.Headers.Unsubscribe.DESTINATION);
String id = (String)headers.get(Stomp.Headers.Unsubscribe.ID);
String subscriptionID = null;
if (id != null)
{
subscriptionID = id;
}
else
{
if (destination == null)
{
throw new StompException("Must specify the subscription's id or the destination you are unsubscribing from");
}
subscriptionID = "subscription/" + destination;
}
StompSession stompSession = getSession(connection);
boolean unsubscribed = stompSession.unsubscribe(subscriptionID);
if (!unsubscribed)
{
throw new StompException("Cannot unsubscribe as no subscription exists for id: " + subscriptionID);
}
return null;
}
private StompFrame onAck(StompFrame frame, StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String messageID = (String)headers.get(Stomp.Headers.Ack.MESSAGE_ID);
String txID = (String)headers.get(Stomp.Headers.TRANSACTION);
StompSession stompSession = null;
if (txID != null)
{
log.warn("Transactional acknowledgement is not supported");
}
stompSession = getSession(connection);
stompSession.acknowledge(messageID);
return null;
}
private StompFrame onBegin(StompFrame frame, HornetQServer server, StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String txID = (String)headers.get(Stomp.Headers.TRANSACTION);
if (txID == null)
{
throw new StompException("transaction header is mandatory to BEGIN a transaction");
}
if (transactedSessions.containsKey(txID))
{
throw new StompException("Transaction already started: " + txID);
}
// create the transacted session
getTransactedSession(connection, txID);
return null;
}
private StompFrame onCommit(StompFrame frame, StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String txID = (String)headers.get(Stomp.Headers.TRANSACTION);
if (txID == null)
{
throw new StompException("transaction header is mandatory to COMMIT a transaction");
}
StompSession session = getTransactedSession(connection, txID);
if (session == null)
{
throw new StompException("No transaction started: " + txID);
}
transactedSessions.remove(txID);
session.getSession().commit();
return null;
}
private StompFrame onAbort(StompFrame frame, StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String txID = (String)headers.get(Stomp.Headers.TRANSACTION);
if (txID == null)
{
throw new StompException("transaction header is mandatory to ABORT a transaction");
}
StompSession session = getTransactedSession(connection, txID);
if (session == null)
{
throw new StompException("No transaction started: " + txID);
}
transactedSessions.remove(txID);
session.getSession().rollback(false);
return null;
}
private void checkConnected(StompConnection connection) throws StompException
{
if (!connection.isValid())
{
throw new StompException("Not connected");
}
}
private StompSession getSession(StompConnection connection) throws Exception
{
StompSession stompSession = sessions.get(connection.getID());
if (stompSession == null)
{
stompSession = new StompSession(connection, this, server.getStorageManager()
.newContext(server.getExecutorFactory().getExecutor()));
String name = UUIDGenerator.getInstance().generateStringUUID();
ServerSession session = server.createSession(name,
connection.getLogin(),
connection.getPasscode(),
HornetQClient.DEFAULT_MIN_LARGE_MESSAGE_SIZE,
connection,
true,
false,
false,
false,
null,
stompSession);
stompSession.setServerSession(session);
sessions.put(connection.getID(), stompSession);
}
server.getStorageManager().setContext(stompSession.getContext());
return stompSession;
}
private StompSession getTransactedSession(StompConnection connection, String txID) throws Exception
{
StompSession stompSession = transactedSessions.get(txID);
if (stompSession == null)
{
stompSession = new StompSession(connection, this, server.getStorageManager().newContext(executor));
String name = UUIDGenerator.getInstance().generateStringUUID();
ServerSession session = server.createSession(name,
connection.getLogin(),
connection.getPasscode(),
HornetQClient.DEFAULT_MIN_LARGE_MESSAGE_SIZE,
connection,
false,
false,
false,
false,
null,
stompSession);
stompSession.setServerSession(session);
transactedSessions.put(txID, stompSession);
}
server.getStorageManager().setContext(stompSession.getContext());
return stompSession;
}
private StompFrame onDisconnect(StompFrame frame, StompConnection connection) throws Exception
{
cleanup(connection);
return null;
}
private StompFrame onSend(StompFrame frame, StompConnection connection) throws Exception
{
checkConnected(connection);
Map<String, Object> headers = frame.getHeaders();
String destination = (String)headers.remove(Stomp.Headers.Send.DESTINATION);
String txID = (String)headers.remove(Stomp.Headers.TRANSACTION);
long timestamp = System.currentTimeMillis();
SimpleString address = SimpleString.toSimpleString(destination);
ServerMessageImpl message = new ServerMessageImpl(server.getStorageManager().generateUniqueID(), 512);
message.setTimestamp(timestamp);
message.setAddress(address);
validateDestination(address);
StompUtils.copyStandardHeadersFromFrameToMessage(frame, message);
if (headers.containsKey(Stomp.Headers.CONTENT_LENGTH))
{
message.setType(Message.BYTES_TYPE);
message.getBodyBuffer().writeBytes(frame.getContent());
}
else
{
message.setType(Message.TEXT_TYPE);
String text = new String(frame.getContent(), "UTF-8");
message.getBodyBuffer().writeNullableSimpleString(SimpleString.toSimpleString(text));
}
StompSession stompSession = null;
if (txID == null)
{
stompSession = getSession(connection);
}
else
{
stompSession = getTransactedSession(connection, txID);
}
if (stompSession.isNoLocal())
{
message.putStringProperty(CONNECTION_ID_PROP, connection.getID().toString());
}
stompSession.getSession().send(message, true);
return null;
}
/**
* @param address
* @throws Exception
* @throws HornetQException
*/
private void validateDestination(SimpleString address) throws Exception, HornetQException
{
if ((address.startsWith(JMS_QUEUE_PREFIX) || address.startsWith(JMS_TOPIC_PREFIX)) && !address.equals(managementAddress))
{
Bindings binding = server.getPostOffice().lookupBindingsForAddress(address);
if (binding == null || binding.getBindings().size() == 0)
{
throw new HornetQException(HornetQException.ADDRESS_DOES_NOT_EXIST, "Address " + address +
" has not been deployed");
}
}
}
private StompFrame onConnect(StompFrame frame, final StompConnection connection) throws Exception
{
Map<String, Object> headers = frame.getHeaders();
String login = (String)headers.get(Stomp.Headers.Connect.LOGIN);
String passcode = (String)headers.get(Stomp.Headers.Connect.PASSCODE);
String clientID = (String)headers.get(Stomp.Headers.Connect.CLIENT_ID);
String requestID = (String)headers.get(Stomp.Headers.Connect.REQUEST_ID);
HornetQSecurityManager sm = server.getSecurityManager();
// The sm will be null case security is not enabled...
if (sm != null)
{
sm.validateUser(login, passcode);
}
connection.setLogin(login);
connection.setPasscode(passcode);
connection.setClientID(clientID);
connection.setValid(true);
HashMap<String, Object> h = new HashMap<String, Object>();
h.put(Stomp.Headers.Connected.SESSION, connection.getID());
if (requestID != null)
{
h.put(Stomp.Headers.Connected.RESPONSE_ID, requestID);
}
return new StompFrame(Stomp.Responses.CONNECTED, h);
}
public void cleanup(final StompConnection connection)
{
connection.setValid(false);
// Close the session outside of the lock on the StompConnection, otherwise it could dead lock
this.executor.execute(new Runnable()
{
public void run()
{
StompSession session = sessions.remove(connection.getID());
if (session != null)
{
try
{
session.getSession().rollback(true);
session.getSession().close(false);
}
catch (Exception e)
{
log.warn(e.getMessage(), e);
}
}
// removed the transacted session belonging to the connection
Iterator<Entry<String, StompSession>> iterator = transactedSessions.entrySet().iterator();
while (iterator.hasNext())
{
Map.Entry<String, StompSession> entry = (Map.Entry<String, StompSession>)iterator.next();
if (entry.getValue().getConnection() == connection)
{
ServerSession serverSession = entry.getValue().getSession();
try
{
serverSession.rollback(true);
serverSession.close(false);
}
catch (Exception e)
{
log.warn(e.getMessage(), e);
}
iterator.remove();
}
}
}
});
}
private void sendReply(final StompConnection connection, final StompFrame frame)
{
server.getStorageManager().afterCompleteOperations(new IOAsyncTask()
{
public void onError(final int errorCode, final String errorMessage)
{
log.warn("Error processing IOCallback code = " + errorCode + " message = " + errorMessage);
StompFrame error = createError(new HornetQException(errorCode, errorMessage), frame);
send(connection, error);
}
public void done()
{
send(connection, frame);
}
});
}
// Inner classes -------------------------------------------------
}