package org.openamq.client.protocol; import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap; import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.mina.common.IoSession; import org.apache.mina.common.WriteFuture; import org.apache.mina.common.IdleStatus; import org.apache.mina.common.CloseFuture; import org.openamq.AMQException; import org.openamq.client.AMQConnection; import org.openamq.client.AMQSession; import org.openamq.client.ConnectionTuneParameters; import org.openamq.client.message.UnprocessedMessage; import org.openamq.client.message.UnexpectedBodyReceivedException; import org.openamq.framing.*; import javax.jms.JMSException; import javax.security.sasl.SaslClient; /** * Wrapper for protocol session that provides type-safe access to session attributes. * * The underlying protocol session is still available but clients should not * use it to obtain session attributes. * */ public class AMQProtocolSession { private static final Logger _logger = LoggerFactory.getLogger(AMQProtocolSession.class); public static final String PROTOCOL_INITIATION_RECEIVED = "ProtocolInitiatiionReceived"; protected static final String CONNECTION_TUNE_PARAMETERS = "ConnectionTuneParameters"; private static final String AMQ_CONNECTION = "AMQConnection"; private static final String SASL_CLIENT = "SASLClient"; private final IoSession _minaProtocolSession; /** * The handler from which this session was created and which is used to handle protocol events. * We send failover events to the handler. */ private final AMQProtocolHandler _protocolHandler; /** * Maps from the channel id to the AMQSession that it represents. */ private ConcurrentMap _channelId2SessionMap = new ConcurrentHashMap(); private ConcurrentMap _closingChannels = new ConcurrentHashMap(); /** * Maps from a channel id to an unprocessed message. This is used to tie together the * JmsDeliverBody (which arrives first) with the subsequent content header and content bodies. */ private ConcurrentMap _channelId2UnprocessedMsgMap = new ConcurrentHashMap(); /** * Counter to ensure unique queue names */ private int _queueId = 1; private final Object _queueIdLock = new Object(); public AMQProtocolSession(AMQProtocolHandler protocolHandler, IoSession protocolSession, AMQConnection connection) { _protocolHandler = protocolHandler; _minaProtocolSession = protocolSession; // properties of the connection are made available to the event handlers _minaProtocolSession.setAttribute(AMQ_CONNECTION, connection); } public void init() { // start the process of setting up the connection. This is the first place that // data is written to the server. _minaProtocolSession.write(new ProtocolInitiation()); } public String getClientID() { try { return getAMQConnection().getClientID(); } catch (JMSException e) { // we never throw a JMSException here return null; } } public void setClientID(String clientID) throws JMSException { getAMQConnection().setClientID(clientID); } public String getVirtualPath() { return getAMQConnection().getVirtualPath(); } public String getUsername() { return getAMQConnection().getUsername(); } public String getPassword() { return getAMQConnection().getPassword(); } public IoSession getIoSession() { return _minaProtocolSession; } public SaslClient getSaslClient() { return (SaslClient) _minaProtocolSession.getAttribute(SASL_CLIENT); } /** * Store the SASL client currently being used for the authentication handshake * @param client if non-null, stores this in the session. if null clears any existing client * being stored */ public void setSaslClient(SaslClient client) { if (client == null) { _minaProtocolSession.removeAttribute(SASL_CLIENT); } else { _minaProtocolSession.setAttribute(SASL_CLIENT, client); } } public ConnectionTuneParameters getConnectionTuneParameters() { return (ConnectionTuneParameters) _minaProtocolSession.getAttribute(CONNECTION_TUNE_PARAMETERS); } public void setConnectionTuneParameters(ConnectionTuneParameters params) { _minaProtocolSession.setAttribute(CONNECTION_TUNE_PARAMETERS, params); AMQConnection con = getAMQConnection(); con.setMaximumChannelCount(params.getChannelMax()); con.setMaximumFrameSize(params.getFrameMax()); initHeartbeats((int) params.getHeartbeat()); } /** * Callback invoked from the BasicDeliverMethodHandler when a message has been received. * This is invoked on the MINA dispatcher thread. * @param message * @throws AMQException if this was not expected */ public void unprocessedMessageReceived(UnprocessedMessage message) throws AMQException { _channelId2UnprocessedMsgMap.put(new Integer(message.channelId), message); } public void messageContentHeaderReceived(int channelId, ContentHeaderBody contentHeader) throws AMQException { UnprocessedMessage msg = (UnprocessedMessage) _channelId2UnprocessedMsgMap.get(new Integer(channelId)); if (msg == null) { throw new AMQException("Error: received content header without having received a JMSDeliver frame first"); } if (msg.contentHeader != null) { throw new AMQException("Error: received duplicate content header or did not receive correct number of content body frames"); } msg.contentHeader = contentHeader; if (contentHeader.bodySize == 0) { deliverMessageToAMQSession(channelId, msg); } } public void messageContentBodyReceived(int channelId, ContentBody contentBody) throws AMQException { UnprocessedMessage msg = (UnprocessedMessage) _channelId2UnprocessedMsgMap.get(new Integer(channelId)); if (msg == null) { throw new AMQException("Error: received content body without having received a JMSDeliver frame first"); } if (msg.contentHeader == null) { _channelId2UnprocessedMsgMap.remove(new Integer(channelId)); throw new AMQException("Error: received content body without having received a ContentHeader frame first"); } try { msg.receiveBody(contentBody); } catch (UnexpectedBodyReceivedException e) { _channelId2UnprocessedMsgMap.remove(new Integer(channelId)); throw e; } if (msg.isAllBodyDataReceived()) { deliverMessageToAMQSession(channelId, msg); } } /** * Deliver a message to the appropriate session, removing the unprocessed message * from our map * @param channelId the channel id the message should be delivered to * @param msg the message */ private void deliverMessageToAMQSession(int channelId, UnprocessedMessage msg) { AMQSession session = (AMQSession) _channelId2SessionMap.get(new Integer(channelId)); session.messageReceived(msg); _channelId2UnprocessedMsgMap.remove(new Integer(channelId)); } /** * Convenience method that writes a frame to the protocol session. Equivalent * to calling getProtocolSession().write(). * * @param frame the frame to write */ public void writeFrame(AMQDataBlock frame) { _minaProtocolSession.write(frame); } public void writeFrame(AMQDataBlock frame, boolean wait) { WriteFuture f =_minaProtocolSession.write(frame); if(wait) { f.join(); } } public void addSessionByChannel(int channelId, AMQSession session) { if (channelId <=0) { throw new IllegalArgumentException("Attempt to register a session with a channel id <= zero"); } if (session == null) { throw new IllegalArgumentException("Attempt to register a null session"); } _logger.debug("Add session with channel id " + channelId); _channelId2SessionMap.put(new Integer(channelId), session); } public void removeSessionByChannel(int channelId) { if (channelId <=0) { throw new IllegalArgumentException("Attempt to deregister a session with a channel id <= zero"); } _logger.debug("Removing session with channelId " + channelId); _channelId2SessionMap.remove(new Integer(channelId)); } /** * Starts the process of closing a session * @param session the AMQSession being closed */ public void closeSession(AMQSession session) { _logger.debug("closeSession called on protocol session for session " + session.getChannelId()); final int channelId = session.getChannelId(); if (channelId <=0) { throw new IllegalArgumentException("Attempt to close a channel with id < 0"); } // we need to know when a channel is closing so that we can respond // with a channel.close frame when we receive any other type of frame // on that channel _closingChannels.putIfAbsent(new Integer(channelId), session); final AMQFrame frame = ChannelCloseBody.createAMQFrame(channelId, AMQConstant.REPLY_SUCCESS.getCode(), "JMS client closing channel", 0, 0); writeFrame(frame); } /** * Called from the ChannelClose handler when a channel close frame is received. * This method decides whether this is a response or an initiation. The latter * case causes the AMQSession to be closed and an exception to be thrown if * appropriate. * @param channelId the id of the channel (session) * @return true if the client must respond to the server, i.e. if the server * initiated the channel close, false if the channel close is just the server * responding to the client's earlier request to close the channel. */ public boolean channelClosed(int channelId, int code, String text) { final Integer chId = new Integer(channelId); // if this is not a response to an earlier request to close the channel if (_closingChannels.remove(chId) == null) { final AMQSession session = (AMQSession) _channelId2SessionMap.get(chId); session.closed(new AMQException(_logger, code, text)); return true; } else { return false; } } public AMQConnection getAMQConnection() { return (AMQConnection) _minaProtocolSession.getAttribute(AMQ_CONNECTION); } public void closeProtocolSession() { _logger.debug("Closing protocol session"); final CloseFuture future = _minaProtocolSession.close(); future.join(); } public void failover(String host, int port) { _protocolHandler.failover(host, port); } String generateQueueName() { int id; synchronized(_queueIdLock) { id = _queueId++; } return "tmp_" + _minaProtocolSession.getLocalAddress() + "_" + id; } /** * * @param delay delay in seconds (not ms) */ void initHeartbeats(int delay) { if(delay > 0) { _minaProtocolSession.setIdleTime(IdleStatus.WRITER_IDLE, delay); _minaProtocolSession.setIdleTime(IdleStatus.READER_IDLE, HeartbeatConfig.CONFIG.getTimeout(delay)); HeartbeatDiagnostics.init(delay, HeartbeatConfig.CONFIG.getTimeout(delay)); } } }