diff options
Diffstat (limited to 'java/client/src')
90 files changed, 12078 insertions, 0 deletions
diff --git a/java/client/src/log4j.properties b/java/client/src/log4j.properties new file mode 100644 index 0000000000..d3135ff574 --- /dev/null +++ b/java/client/src/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootLogger=${root.logging.level} + + +log4j.logger.org.apache.qpid=${amqj.logging.level}, console +log4j.additivity.org.apache.qpid=false + +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.Threshold=info +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=%t %d %p [%c{4}] %m%n diff --git a/java/client/src/org/apache/qpid/client/AMQAuthenticationException.java b/java/client/src/org/apache/qpid/client/AMQAuthenticationException.java new file mode 100644 index 0000000000..93b5cb5b8e --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQAuthenticationException.java @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.AMQException; +import org.apache.qpid.protocol.AMQConstant; + +public class AMQAuthenticationException extends AMQException +{ + public AMQAuthenticationException(int error, String msg) + { + super(error,msg); + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQBrokerDetails.java b/java/client/src/org/apache/qpid/client/AMQBrokerDetails.java new file mode 100644 index 0000000000..52de858b13 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQBrokerDetails.java @@ -0,0 +1,301 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.url.URLHelper; +import org.apache.qpid.url.URLSyntaxException; + +import java.util.HashMap; +import java.net.URISyntaxException; +import java.net.URI; + +public class AMQBrokerDetails implements BrokerDetails +{ + private String _host; + private int _port; + private String _transport; + + private HashMap<String, String> _options; + + public AMQBrokerDetails() + { + _options = new HashMap<String, String>(); + } + + public AMQBrokerDetails(String url) throws URLSyntaxException + { + this(); + // URL should be of format tcp://host:port?option='value',option='value' + try + { + URI connection = new URI(url); + + String transport = connection.getScheme(); + + // Handles some defaults to minimise changes to existing broker URLS e.g. localhost + if (transport != null) + { + //todo this list of valid transports should be enumerated somewhere + if ((!(transport.equalsIgnoreCase("vm") || + transport.equalsIgnoreCase("tcp")))) + { + if (transport.equalsIgnoreCase("localhost")) + { + connection = new URI(DEFAULT_TRANSPORT + "://" + url); + transport = connection.getScheme(); + } + else + { + if (url.charAt(transport.length()) == ':' && url.charAt(transport.length()+1) != '/' ) + { + //Then most likely we have a host:port value + connection = new URI(DEFAULT_TRANSPORT + "://" + url); + transport = connection.getScheme(); + } + else + { + URLHelper.parseError(0, transport.length(), "Unknown transport", url); + } + } + } + } + else + { + //Default the transport + connection = new URI(DEFAULT_TRANSPORT + "://" + url); + transport = connection.getScheme(); + } + + if (transport == null) + { + URLHelper.parseError(-1, "Unknown transport:'" + transport + "'" + + " In broker URL:'" + url + "' Format: " + URL_FORMAT_EXAMPLE, ""); + } + + setTransport(transport); + + String host = connection.getHost(); + + // Fix for Java 1.5 + if (host == null) + { + host = ""; + } + + setHost(host); + + int port = connection.getPort(); + + if (port == -1) + { + // Another fix for Java 1.5 URI handling + String auth = connection.getAuthority(); + + if (auth != null && auth.startsWith(":")) + { + setPort(Integer.parseInt(auth.substring(1))); + } + else + { + setPort(DEFAULT_PORT); + } + } + else + { + setPort(port); + } + + String queryString = connection.getQuery(); + + URLHelper.parseOptions(_options, queryString); + + //Fragment is #string (not used) + } + catch (URISyntaxException uris) + { + if (uris instanceof URLSyntaxException) + { + throw (URLSyntaxException) uris; + } + + URLHelper.parseError(uris.getIndex(), uris.getReason(), uris.getInput()); + } + } + + public AMQBrokerDetails(String host, int port, boolean useSSL) + { + _host = host; + _port = port; + + if (useSSL) + { + setOption(OPTIONS_SSL, "true"); + } + } + + public String getHost() + { + return _host; + } + + public void setHost(String _host) + { + this._host = _host; + } + + public int getPort() + { + return _port; + } + + public void setPort(int _port) + { + this._port = _port; + } + + public String getTransport() + { + return _transport; + } + + public void setTransport(String _transport) + { + this._transport = _transport; + } + + + public String getOption(String key) + { + return _options.get(key); + } + + public void setOption(String key, String value) + { + _options.put(key, value); + } + + public long getTimeout() + { + if (_options.containsKey(OPTIONS_CONNECT_TIMEOUT)) + { + try + { + return Long.parseLong(_options.get(OPTIONS_CONNECT_TIMEOUT)); + } + catch (NumberFormatException nfe) + { + //Do nothing as we will use the default below. + } + } + + return BrokerDetails.DEFAULT_CONNECT_TIMEOUT; + } + + public void setTimeout(long timeout) + { + setOption(OPTIONS_CONNECT_TIMEOUT, Long.toString(timeout)); + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + + sb.append(_transport); + sb.append("://"); + + if (!(_transport.equalsIgnoreCase("vm"))) + { + sb.append(_host); + } + + sb.append(':'); + sb.append(_port); + + sb.append(printOptionsURL()); + + return sb.toString(); + } + + public boolean equals(Object o) + { + if (!(o instanceof BrokerDetails)) + { + return false; + } + + BrokerDetails bd = (BrokerDetails) o; + + return _host.equalsIgnoreCase(bd.getHost()) && + (_port == bd.getPort()) && + _transport.equalsIgnoreCase(bd.getTransport()) && + (useSSL() == bd.useSSL()); + + //todo do we need to compare all the options as well? + } + + private String printOptionsURL() + { + StringBuffer optionsURL = new StringBuffer(); + + optionsURL.append('?'); + + if (!(_options.isEmpty())) + { + + for (String key : _options.keySet()) + { + optionsURL.append(key); + + optionsURL.append("='"); + + optionsURL.append(_options.get(key)); + + optionsURL.append("'"); + + optionsURL.append(URLHelper.DEFAULT_OPTION_SEPERATOR); + } + } + + //remove the extra DEFAULT_OPTION_SEPERATOR or the '?' if there are no options + optionsURL.deleteCharAt(optionsURL.length() - 1); + + return optionsURL.toString(); + } + + public boolean useSSL() + { + // To be friendly to users we should be case insensitive. + // or simply force users to conform to OPTIONS_SSL + // todo make case insensitive by trying ssl Ssl sSl ssL SSl SsL sSL SSL + + if (_options.containsKey(OPTIONS_SSL)) + { + return _options.get(OPTIONS_SSL).equalsIgnoreCase("true"); + } + + return false; + } + + public void useSSL(boolean ssl) + { + setOption(OPTIONS_SSL, Boolean.toString(ssl)); + } + + +} diff --git a/java/client/src/org/apache/qpid/client/AMQConnection.java b/java/client/src/org/apache/qpid/client/AMQConnection.java new file mode 100644 index 0000000000..f11d4d9307 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQConnection.java @@ -0,0 +1,927 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.AMQException; +import org.apache.qpid.AMQUndeliveredException; +import org.apache.qpid.AMQConnectionException; +import org.apache.qpid.AMQUnresolvedAddressException; +import org.apache.qpid.url.URLSyntaxException; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.client.failover.FailoverSupport; +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.transport.TransportConnection; +import org.apache.qpid.framing.*; +import org.apache.qpid.jms.*; +import org.apache.qpid.jms.Connection; + +import org.apache.log4j.Logger; + +import javax.jms.*; +import javax.jms.Queue; +import javax.jms.Session; +import javax.naming.Reference; +import javax.naming.NamingException; +import javax.naming.StringRefAddr; +import javax.naming.Referenceable; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.net.ConnectException; +import java.nio.channels.UnresolvedAddressException; +import java.text.MessageFormat; + +public class AMQConnection extends Closeable implements Connection, QueueConnection, TopicConnection, Referenceable +{ + private static final Logger _logger = Logger.getLogger(AMQConnection.class); + + private AtomicInteger _idFactory = new AtomicInteger(0); + + /** + * This is the "root" mutex that must be held when doing anything that could be impacted by failover. + * This must be held by any child objects of this connection such as the session, producers and consumers. + */ + private final Object _failoverMutex = new Object(); + + /** + * A channel is roughly analogous to a session. The server can negotiate the maximum number of channels + * per session and we must prevent the client from opening too many. Zero means unlimited. + */ + private long _maximumChannelCount; + + /** + * The maximum size of frame supported by the server + */ + private long _maximumFrameSize; + + /** + * The protocol handler dispatches protocol events for this connection. For example, when the connection is dropped + * the handler deals with this. It also deals with the initial dispatch of any protocol frames to their appropriate + * handler. + */ + private AMQProtocolHandler _protocolHandler; + + /** + * Maps from session id (Integer) to AMQSession instance + */ + private final Map _sessions = new LinkedHashMap(); //fixme this is map is replicated in amqprotocolsession as _channelId2SessionMap + + private String _clientName; + + /** + * The user name to use for authentication + */ + private String _username; + + /** + * The password to use for authentication + */ + private String _password; + + /** + * The virtual path to connect to on the AMQ server + */ + private String _virtualHost; + + private ExceptionListener _exceptionListener; + + private ConnectionListener _connectionListener; + + private ConnectionURL _connectionURL; + + /** + * Whether this connection is started, i.e. whether messages are flowing to consumers. It has no meaning for + * message publication. + */ + private boolean _started; + + /** + * Policy dictating how to failover + */ + private FailoverPolicy _failoverPolicy; + + /* + * _Connected should be refactored with a suitable wait object. + */ + private boolean _connected; + + /* + * The last error code that occured on the connection. Used to return the correct exception to the client + */ + private AMQException _lastAMQException = null; + + public AMQConnection(String broker, String username, String password, + String clientName, String virtualHost) throws AMQException, URLSyntaxException + { + this(new AMQConnectionURL(ConnectionURL.AMQ_PROTOCOL + "://" + + username + ":" + password + "@" + clientName + + virtualHost + "?brokerlist='" + broker + "'")); + } + + public AMQConnection(String host, int port, String username, String password, + String clientName, String virtualHost) throws AMQException, URLSyntaxException + { + this(host, port, false, username, password, clientName, virtualHost); + } + + public AMQConnection(String host, int port, boolean useSSL, String username, String password, + String clientName, String virtualHost) throws AMQException, URLSyntaxException + { + this(new AMQConnectionURL(useSSL ? + ConnectionURL.AMQ_PROTOCOL + "://" + + username + ":" + password + "@" + clientName + + virtualHost + "?brokerlist='tcp://" + host + ":" + port + "'" + + "," + ConnectionURL.OPTIONS_SSL + "='true'" : + ConnectionURL.AMQ_PROTOCOL + "://" + + username + ":" + password + "@" + clientName + + virtualHost + "?brokerlist='tcp://" + host + ":" + port + "'" + + "," + ConnectionURL.OPTIONS_SSL + "='false'" + )); + } + + public AMQConnection(String connection) throws AMQException, URLSyntaxException + { + this(new AMQConnectionURL(connection)); + } + + public AMQConnection(ConnectionURL connectionURL) throws AMQException + { + _logger.info("Connection:" + connectionURL); + + if (connectionURL == null) + { + throw new IllegalArgumentException("Connection must be specified"); + } + + _connectionURL = connectionURL; + + _clientName = connectionURL.getClientName(); + _username = connectionURL.getUsername(); + _password = connectionURL.getPassword(); + _virtualHost = connectionURL.getVirtualHost(); + + _failoverPolicy = new FailoverPolicy(connectionURL); + + _protocolHandler = new AMQProtocolHandler(this); + + // We are not currently connected + _connected = false; + + + Exception lastException = new Exception(); + lastException.initCause(new ConnectException()); + + while (lastException != null && lastException.getCause() instanceof ConnectException && _failoverPolicy.failoverAllowed()) + { + try + { + makeBrokerConnection(_failoverPolicy.getNextBrokerDetails()); + lastException = null; + } + catch (Exception e) + { + lastException = e; + + _logger.info("Unable to connect to broker at " + _failoverPolicy.getCurrentBrokerDetails(), e.getCause()); + _logger.info(e); + _logger.info(e.getCause()); + } + } + + _logger.debug("Are we connected:" + _connected); + + // Then the Failover Thread will handle conneciton + if (_failoverPolicy.failoverAllowed()) + { + //TODO this needs to be redone so that we are not spinning. + // A suitable object should be set that is then waited on + // and only notified when a connection is made or when + // the AMQConnection gets closed. + while (!_connected && !_closed.get()) + { + try + { + _logger.debug("Sleeping."); + Thread.sleep(100); + } + catch (InterruptedException ie) + { + _logger.debug("Woken up."); + } + } + if (!_failoverPolicy.failoverAllowed() || _failoverPolicy.getCurrentBrokerDetails() == null) + { + if (_lastAMQException != null) + { + throw _lastAMQException; + } + } + } + else + { + String message = null; + + if (lastException != null) + { + if (lastException.getCause() != null) + { + message = lastException.getCause().getMessage(); + } + else + { + message = lastException.getMessage(); + } + } + + if (message == null || message.equals("")) + { + message = "Unable to Connect"; + } + + AMQException e = new AMQConnectionException(message); + + if (lastException != null) + { + if (lastException instanceof UnresolvedAddressException) + { + e = new AMQUnresolvedAddressException(message); + } + e.initCause(lastException); + } + + throw e; + } + } + + protected AMQConnection(String username, String password, String clientName, String virtualHost) + { + _clientName = clientName; + _username = username; + _password = password; + _virtualHost = virtualHost; + } + + private void makeBrokerConnection(BrokerDetails brokerDetail) throws IOException, AMQException + { + try + { + TransportConnection.getInstance().connect(_protocolHandler, brokerDetail); + // this blocks until the connection has been set up or when an error + // has prevented the connection being set up + _protocolHandler.attainState(AMQState.CONNECTION_OPEN); + _failoverPolicy.attainedConnection(); + + //Again this should be changed to a suitable notify + _connected = true; + } + catch (AMQException e) + { + _lastAMQException = e; + throw e; + } + } + + public boolean attemptReconnection(String host, int port, boolean useSSL) + { + BrokerDetails bd = new AMQBrokerDetails(host, port, useSSL); + + _failoverPolicy.setBroker(bd); + + try + { + makeBrokerConnection(bd); + return true; + } + catch (Exception e) + { + _logger.info("Unable to connect to broker at " + bd); + attemptReconnection(); + } + return false; + } + + public boolean attemptReconnection() + { + while (_failoverPolicy.failoverAllowed()) + { + try + { + makeBrokerConnection(_failoverPolicy.getNextBrokerDetails()); + return true; + } + catch (Exception e) + { + if (!(e instanceof AMQException)) + { + _logger.info("Unable to connect to broker at " + _failoverPolicy.getCurrentBrokerDetails(), e); + } + else + { + _logger.info(e.getMessage() + ":Unable to connect to broker at " + _failoverPolicy.getCurrentBrokerDetails()); + } + } + } + + //connection unsuccessful + return false; + } + + /** + * Get the details of the currently active broker + * + * @return null if no broker is active (i.e. no successful connection has been made, or + * the BrokerDetail instance otherwise + */ + public BrokerDetails getActiveBrokerDetails() + { + return _failoverPolicy.getCurrentBrokerDetails(); + } + + public boolean failoverAllowed() + { + return _failoverPolicy.failoverAllowed(); + } + + public Session createSession(final boolean transacted, final int acknowledgeMode) throws JMSException + { + return createSession(transacted, acknowledgeMode, AMQSession.DEFAULT_PREFETCH); + } + + public org.apache.qpid.jms.Session createSession(final boolean transacted, final int acknowledgeMode, + final int prefetch) throws JMSException + { + checkNotClosed(); + if (channelLimitReached()) + { + throw new ChannelLimitReachedException(_maximumChannelCount); + } + else + { + return (org.apache.qpid.jms.Session) new FailoverSupport() + { + public Object operation() throws JMSException + { + int channelId = _idFactory.incrementAndGet(); + + if (_logger.isDebugEnabled()) + { + _logger.debug("Write channel open frame for channel id " + channelId); + } + + // We must create the session and register it before actually sending the frame to the server to + // open it, so that there is no window where we could receive data on the channel and not be set + // up to handle it appropriately. + AMQSession session = new AMQSession(AMQConnection.this, channelId, transacted, acknowledgeMode, + prefetch); + _protocolHandler.addSessionByChannel(channelId, session); + registerSession(channelId, session); + + boolean success = false; + try + { + createChannelOverWire(channelId, prefetch, transacted); + success = true; + } + catch (AMQException e) + { + JMSException jmse = new JMSException("Error creating session: " + e); + jmse.setLinkedException(e); + throw jmse; + } + finally + { + if (!success) { + _protocolHandler.removeSessionByChannel(channelId); + deregisterSession(channelId); + } + } + + if (_started) + { + session.start(); + } + return session; + } + }.execute(this); + } + } + + private void createChannelOverWire(int channelId, int prefetch, boolean transacted) + throws AMQException + { + _protocolHandler.syncWrite( + ChannelOpenBody.createAMQFrame(channelId, null), ChannelOpenOkBody.class); + _protocolHandler.syncWrite( + BasicQosBody.createAMQFrame(channelId, 0, prefetch, false), + BasicQosOkBody.class); + + if (transacted) + { + if (_logger.isDebugEnabled()) + { + _logger.debug("Issuing TxSelect for " + channelId); + } + _protocolHandler.syncWrite(TxSelectBody.createAMQFrame(channelId), TxSelectOkBody.class); + } + } + + private void reopenChannel(int channelId, int prefetch, boolean transacted) throws AMQException + { + try + { + createChannelOverWire(channelId, prefetch, transacted); + } + catch (AMQException e) + { + _protocolHandler.removeSessionByChannel(channelId); + deregisterSession(channelId); + throw new AMQException("Error reopening channel " + channelId + " after failover: " + e); + } + } + + + public void setFailoverPolicy(FailoverPolicy policy) + { + _failoverPolicy = policy; + } + + public FailoverPolicy getFailoverPolicy() + { + return _failoverPolicy; + } + + public QueueSession createQueueSession(boolean transacted, int acknowledgeMode) throws JMSException + { + return (QueueSession) createSession(transacted, acknowledgeMode); + } + + public TopicSession createTopicSession(boolean transacted, int acknowledgeMode) throws JMSException + { + return (TopicSession) createSession(transacted, acknowledgeMode); + } + + private boolean channelLimitReached() + { + return _maximumChannelCount != 0 && _sessions.size() == _maximumChannelCount; + } + + public String getClientID() throws JMSException + { + checkNotClosed(); + return _clientName; + } + + public void setClientID(String clientID) throws JMSException + { + checkNotClosed(); + _clientName = clientID; + } + + public ConnectionMetaData getMetaData() throws JMSException + { + checkNotClosed(); + // TODO Auto-generated method stub + return null; + } + + public ExceptionListener getExceptionListener() throws JMSException + { + checkNotClosed(); + return _exceptionListener; + } + + public void setExceptionListener(ExceptionListener listener) throws JMSException + { + checkNotClosed(); + _exceptionListener = listener; + } + + /** + * Start the connection, i.e. start flowing messages. Note that this method must be called only from a single thread + * and is not thread safe (which is legal according to the JMS specification). + * + * @throws JMSException + */ + public void start() throws JMSException + { + checkNotClosed(); + if (!_started) + { + final Iterator it = _sessions.entrySet().iterator(); + while (it.hasNext()) + { + final AMQSession s = (AMQSession) ((Map.Entry) it.next()).getValue(); + s.start(); + } + _started = true; + } + } + + public void stop() throws JMSException + { + checkNotClosed(); + + if (_started) + { + for (Iterator i = _sessions.values().iterator(); i.hasNext();) + { + ((AMQSession) i.next()).stop(); + } + _started = false; + } + } + + public void close() throws JMSException + { + synchronized (getFailoverMutex()) + { + if (!_closed.getAndSet(true)) + { + try + { + closeAllSessions(null); + _protocolHandler.closeConnection(); + } + catch (AMQException e) + { + throw new JMSException("Error closing connection: " + e); + } + } + } + } + + /** + * Marks all sessions and their children as closed without sending any protocol messages. Useful when + * you need to mark objects "visible" in userland as closed after failover or other significant event that + * impacts the connection. + * <p/> + * The caller must hold the failover mutex before calling this method. + */ + private void markAllSessionsClosed() + { + final LinkedList sessionCopy = new LinkedList(_sessions.values()); + final Iterator it = sessionCopy.iterator(); + while (it.hasNext()) + { + final AMQSession session = (AMQSession) it.next(); + + session.markClosed(); + } + _sessions.clear(); + } + + /** + * Close all the sessions, either due to normal connection closure or due to an error occurring. + * + * @param cause if not null, the error that is causing this shutdown + * <p/> + * The caller must hold the failover mutex before calling this method. + */ + private void closeAllSessions(Throwable cause) throws JMSException + { + final LinkedList sessionCopy = new LinkedList(_sessions.values()); + final Iterator it = sessionCopy.iterator(); + JMSException sessionException = null; + while (it.hasNext()) + { + final AMQSession session = (AMQSession) it.next(); + if (cause != null) + { + session.closed(cause); + } + else + { + try + { + session.close(); + } + catch (JMSException e) + { + _logger.error("Error closing session: " + e); + sessionException = e; + } + } + } + _sessions.clear(); + if (sessionException != null) + { + throw sessionException; + } + } + + public ConnectionConsumer createConnectionConsumer(Destination destination, String messageSelector, + ServerSessionPool sessionPool, + int maxMessages) throws JMSException + { + checkNotClosed(); + return null; + } + + public ConnectionConsumer createConnectionConsumer(Queue queue, String messageSelector, + ServerSessionPool sessionPool, + int maxMessages) throws JMSException + { + checkNotClosed(); + return null; + } + + public ConnectionConsumer createConnectionConsumer(Topic topic, String messageSelector, + ServerSessionPool sessionPool, + int maxMessages) throws JMSException + { + checkNotClosed(); + return null; + } + + public ConnectionConsumer createDurableConnectionConsumer(Topic topic, String subscriptionName, + String messageSelector, ServerSessionPool sessionPool, + int maxMessages) + throws JMSException + { + // TODO Auto-generated method stub + checkNotClosed(); + return null; + } + + public long getMaximumChannelCount() + { + checkNotClosed(); + return _maximumChannelCount; + } + + public void setConnectionListener(ConnectionListener listener) + { + _connectionListener = listener; + } + + public ConnectionListener getConnectionListener() + { + return _connectionListener; + } + + public void setMaximumChannelCount(long maximumChannelCount) + { + checkNotClosed(); + _maximumChannelCount = maximumChannelCount; + } + + public void setMaximumFrameSize(long frameMax) + { + _maximumFrameSize = frameMax; + } + + public long getMaximumFrameSize() + { + return _maximumFrameSize; + } + + public Map getSessions() + { + return _sessions; + } + + public String getUsername() + { + return _username; + } + + public String getPassword() + { + return _password; + } + + public String getVirtualHost() + { + return _virtualHost; + } + + public AMQProtocolHandler getProtocolHandler() + { + return _protocolHandler; + } + + public void bytesSent(long writtenBytes) + { + if (_connectionListener != null) + { + _connectionListener.bytesSent(writtenBytes); + } + } + + public void bytesReceived(long receivedBytes) + { + if (_connectionListener != null) + { + _connectionListener.bytesReceived(receivedBytes); + } + } + + /** + * Fire the preFailover event to the registered connection listener (if any) + * + * @param redirect true if this is the result of a redirect request rather than a connection error + * @return true if no listener or listener does not veto change + */ + public boolean firePreFailover(boolean redirect) + { + boolean proceed = true; + if (_connectionListener != null) + { + proceed = _connectionListener.preFailover(redirect); + } + return proceed; + } + + /** + * Fire the preResubscribe event to the registered connection listener (if any). If the listener + * vetoes resubscription then all the sessions are closed. + * + * @return true if no listener or listener does not veto resubscription. + * @throws JMSException + */ + public boolean firePreResubscribe() throws JMSException + { + if (_connectionListener != null) + { + boolean resubscribe = _connectionListener.preResubscribe(); + if (!resubscribe) + { + markAllSessionsClosed(); + } + return resubscribe; + } + else + { + return true; + } + } + + /** + * Fires a failover complete event to the registered connection listener (if any). + */ + public void fireFailoverComplete() + { + if (_connectionListener != null) + { + _connectionListener.failoverComplete(); + } + } + + /** + * In order to protect the consistency of the connection and its child sessions, consumers and producers, + * the "failover mutex" must be held when doing any operations that could be corrupted during failover. + * + * @return a mutex. Guaranteed never to change for the lifetime of this connection even if failover occurs. + */ + public final Object getFailoverMutex() + { + return _failoverMutex; + } + + /** + * If failover is taking place this will block until it has completed. If failover + * is not taking place it will return immediately. + * + * @throws InterruptedException + */ + public void blockUntilNotFailingOver() throws InterruptedException + { + _protocolHandler.blockUntilNotFailingOver(); + } + + /** + * Invoked by the AMQProtocolSession when a protocol session exception has occurred. + * This method sends the exception to a JMS exception listener, if configured, and + * propagates the exception to sessions, which in turn will propagate to consumers. + * This allows synchronous consumers to have exceptions thrown to them. + * + * @param cause the exception + */ + public void exceptionReceived(Throwable cause) + { + + _logger.debug("Connection Close done by:" + Thread.currentThread().getName()); + _logger.debug("exceptionReceived is ", cause); + + final JMSException je; + if (cause instanceof JMSException) + { + je = (JMSException) cause; + } + else + { + je = new JMSException("Exception thrown against " + toString() + ": " + cause); + if (cause instanceof Exception) + { + je.setLinkedException((Exception) cause); + } + } + + // in the case of an IOException, MINA has closed the protocol session so we set _closed to true + // so that any generic client code that tries to close the connection will not mess up this error + // handling sequence + if (cause instanceof IOException) + { + _closed.set(true); + } + + if (_exceptionListener != null) + { + _exceptionListener.onException(je); + } + + if (!(cause instanceof AMQUndeliveredException) && !(cause instanceof AMQAuthenticationException)) + { + try + { + _logger.info("Closing AMQConnection due to :" + cause.getMessage()); + _closed.set(true); + closeAllSessions(cause); // FIXME: when doing this end up with RejectedExecutionException from executor. + } + catch (JMSException e) + { + _logger.error("Error closing all sessions: " + e, e); + } + + } + else + { + _logger.info("Not a hard-error connection not closing."); + } + } + + void registerSession(int channelId, AMQSession session) + { + _sessions.put(channelId, session); + } + + void deregisterSession(int channelId) + { + _sessions.remove(channelId); + } + + /** + * For all sessions, and for all consumers in those sessions, resubscribe. This is called during failover handling. + * The caller must hold the failover mutex before calling this method. + */ + public void resubscribeSessions() throws AMQException + { + ArrayList sessions = new ArrayList(_sessions.values()); + _logger.info(MessageFormat.format("Resubscribing sessions = {0} sessions.size={1}", sessions, sessions.size())); // FIXME: remove? + for (Iterator it = sessions.iterator(); it.hasNext();) + { + AMQSession s = (AMQSession) it.next(); + _protocolHandler.addSessionByChannel(s.getChannelId(), s); + reopenChannel(s.getChannelId(), s.getDefaultPrefetch(), s.getTransacted()); + s.resubscribe(); + } + } + + public String toString() + { + StringBuffer buf = new StringBuffer("AMQConnection:\n"); + if (_failoverPolicy.getCurrentBrokerDetails() == null) + { + buf.append("No active broker connection"); + } + else + { + BrokerDetails bd = _failoverPolicy.getCurrentBrokerDetails(); + buf.append("Host: ").append(String.valueOf(bd.getHost())); + buf.append("\nPort: ").append(String.valueOf(bd.getPort())); + } + buf.append("\nVirtual Host: ").append(String.valueOf(_virtualHost)); + buf.append("\nClient ID: ").append(String.valueOf(_clientName)); + buf.append("\nActive session count: ").append(_sessions == null ? 0 : _sessions.size()); + return buf.toString(); + } + + public String toURL() + { + return _connectionURL.toString(); + } + + public Reference getReference() throws NamingException + { + return new Reference( + AMQConnection.class.getName(), + new StringRefAddr(AMQConnection.class.getName(), toURL()), + AMQConnectionFactory.class.getName(), + null); // factory location + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQConnectionFactory.java b/java/client/src/org/apache/qpid/client/AMQConnectionFactory.java new file mode 100644 index 0000000000..05a484e366 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQConnectionFactory.java @@ -0,0 +1,358 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.AMQException; +import org.apache.qpid.url.AMQBindingURL; +import org.apache.qpid.url.URLSyntaxException; +import org.apache.qpid.jms.ConnectionURL; + +import javax.jms.*; +import javax.jms.Connection; +import javax.naming.*; +import javax.naming.spi.ObjectFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Hashtable; + + +public class AMQConnectionFactory implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, ObjectFactory, Referenceable +{ + private String _host; + private int _port; + private String _defaultUsername; + private String _defaultPassword; + private String _virtualPath; + + private ConnectionURL _connectionDetails; + + + public AMQConnectionFactory() + { + } + + public AMQConnectionFactory(String url) throws URLSyntaxException + { + _connectionDetails = new AMQConnectionURL(url); + } + + public AMQConnectionFactory(ConnectionURL url) + { + _connectionDetails = url; + } + + public AMQConnectionFactory(String broker, String username, String password, + String clientName, String virtualHost) throws URLSyntaxException + { + this(new AMQConnectionURL(ConnectionURL.AMQ_PROTOCOL + "://" + + username + ":" + password + "@" + clientName + + virtualHost + "?brokerlist='" + broker + "'")); + } + + public AMQConnectionFactory(String host, int port, String virtualPath) + { + this(host, port, "guest", "guest", virtualPath); + } + + public AMQConnectionFactory(String host, int port, String defaultUsername, String defaultPassword, + String virtualPath) + { + _host = host; + _port = port; + _defaultUsername = defaultUsername; + _defaultPassword = defaultPassword; + _virtualPath = virtualPath; + +//todo when setting Host/Port has been resolved then we can use this otherwise those methods won't work with the following line. +// _connectionDetails = new AMQConnectionURL( +// ConnectionURL.AMQ_PROTOCOL + "://" + +// _defaultUsername + ":" + _defaultPassword + "@" + +// virtualPath + "?brokerlist='tcp://" + host + ":" + port + "'"); + } + + /** + * @return The _defaultPassword. + */ + public final String getDefaultPassword(String password) + { + if (_connectionDetails != null) + { + return _connectionDetails.getPassword(); + } + else + { + return _defaultPassword; + } + } + + /** + * @param password The _defaultPassword to set. + */ + public final void setDefaultPassword(String password) + { + if (_connectionDetails != null) + { + _connectionDetails.setPassword(password); + } + _defaultPassword = password; + } + + /** + * @return The _defaultPassword. + */ + public final String getDefaultUsername(String password) + { + if (_connectionDetails != null) + { + return _connectionDetails.getUsername(); + } + else + { + return _defaultUsername; + } + } + + /** + * @param username The _defaultUsername to set. + */ + public final void setDefaultUsername(String username) + { + if (_connectionDetails != null) + { + _connectionDetails.setUsername(username); + } + _defaultUsername = username; + } + + /** + * @return The _host . + */ + public final String getHost() + { + //todo this doesn't make sense in a multi broker URL as we have no current as that is done by AMQConnection + return _host; + } + + /** + * @param host The _host to set. + */ + public final void setHost(String host) + { + //todo if _connectionDetails is set then run _connectionDetails.addBrokerDetails() + // Should perhaps have this method changed to setBroker(host,port) + _host = host; + } + + /** + * @return _port The _port to set. + */ + public final int getPort() + { + //todo see getHost + return _port; + } + + /** + * @param port The port to set. + */ + public final void setPort(int port) + { + //todo see setHost + _port = port; + } + + /** + * @return he _virtualPath. + */ + public final String getVirtualPath() + { + if (_connectionDetails != null) + { + return _connectionDetails.getVirtualHost(); + } + else + { + return _virtualPath; + } + } + + /** + * @param path The _virtualPath to set. + */ + public final void setVirtualPath(String path) + { + if (_connectionDetails != null) + { + _connectionDetails.setVirtualHost(path); + } + + _virtualPath = path; + } + + static String getUniqueClientID() + { + try + { + InetAddress addr = InetAddress.getLocalHost(); + return addr.getHostName() + System.currentTimeMillis(); + } + catch (UnknownHostException e) + { + return null; + } + } + + public Connection createConnection() throws JMSException + { + try + { + if (_connectionDetails != null) + { + if (_connectionDetails.getClientName() == null || _connectionDetails.getClientName().equals("")) + { + _connectionDetails.setClientName(getUniqueClientID()); + } + return new AMQConnection(_connectionDetails); + } + else + { + return new AMQConnection(_host, _port, _defaultUsername, _defaultPassword, getUniqueClientID(), + _virtualPath); + } + } + catch (Exception e) + { + JMSException jmse = new JMSException("Error creating connection: " + e.getMessage()); + jmse.setLinkedException(e); + throw jmse; + } + + + } + + public Connection createConnection(String userName, String password) throws JMSException + { + try + { + return new AMQConnection(_host, _port, userName, password, getUniqueClientID(), _virtualPath); + } + catch (Exception e) + { + JMSException jmse = new JMSException("Error creating connection: " + e.getMessage()); + jmse.setLinkedException(e); + throw jmse; + } + } + + public QueueConnection createQueueConnection() throws JMSException + { + return (QueueConnection) createConnection(); + } + + public QueueConnection createQueueConnection(String username, String password) throws JMSException + { + return (QueueConnection) createConnection(username, password); + } + + public TopicConnection createTopicConnection() throws JMSException + { + return (TopicConnection) createConnection(); + } + + public TopicConnection createTopicConnection(String username, String password) throws JMSException + { + return (TopicConnection) createConnection(username, password); + } + + + public ConnectionURL getConnectionURL() + { + return _connectionDetails; + } + + /** + * JNDI interface to create objects from References. + * + * @param obj The Reference from JNDI + * @param name + * @param ctx + * @param env + * @return AMQConnection,AMQTopic,AMQQueue, or AMQConnectionFactory. + * @throws Exception + */ + public Object getObjectInstance(Object obj, Name name, Context ctx, + Hashtable env) throws Exception + { + if (obj instanceof Reference) + { + Reference ref = (Reference) obj; + + if (ref.getClassName().equals(AMQConnection.class.getName())) + { + RefAddr addr = ref.get(AMQConnection.class.getName()); + + if (addr != null) + { + return new AMQConnection((String) addr.getContent()); + } + } + + if (ref.getClassName().equals(AMQQueue.class.getName())) + { + RefAddr addr = ref.get(AMQQueue.class.getName()); + + if (addr != null) + { + return new AMQQueue(new AMQBindingURL((String) addr.getContent()).getQueueName()); + } + } + + if (ref.getClassName().equals(AMQTopic.class.getName())) + { + RefAddr addr = ref.get(AMQTopic.class.getName()); + + if (addr != null) + { + return new AMQTopic(new AMQBindingURL((String) addr.getContent()).getDestinationName()); + } + } + + if (ref.getClassName().equals(AMQConnectionFactory.class.getName())) + { + RefAddr addr = ref.get(AMQConnectionFactory.class.getName()); + + if (addr != null) + { + return new AMQConnectionFactory(new AMQConnectionURL((String) addr.getContent())); + } + } + + } + return null; + } + + + public Reference getReference() throws NamingException + { + return new Reference( + AMQConnectionFactory.class.getName(), + new StringRefAddr(AMQConnectionFactory.class.getName(), _connectionDetails.getURL()), + AMQConnectionFactory.class.getName(), + null); // factory location + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQConnectionURL.java b/java/client/src/org/apache/qpid/client/AMQConnectionURL.java new file mode 100644 index 0000000000..b87388b9c8 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQConnectionURL.java @@ -0,0 +1,399 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.jms.ConnectionURL; +import org.apache.qpid.url.URLHelper; +import org.apache.qpid.url.URLSyntaxException; + +import java.util.*; +import java.net.URI; +import java.net.URISyntaxException; + +public class AMQConnectionURL implements ConnectionURL +{ + private String _url; + private String _failoverMethod; + private HashMap<String, String> _failoverOptions; + private HashMap<String, String> _options; + private List<BrokerDetails> _brokers; + private String _clientName; + private String _username; + private String _password; + private String _virtualHost; + + public AMQConnectionURL(String fullURL) throws URLSyntaxException + { + _url = fullURL; + _options = new HashMap<String, String>(); + _brokers = new LinkedList<BrokerDetails>(); + _failoverOptions = new HashMap<String, String>(); + + // Connection URL format + //amqp://[user:pass@][clientid]/virtualhost?brokerlist='tcp://host:port?option=\'value\',option=\'value\';vm://:3/virtualpath?option=\'value\'',failover='method?option=\'value\',option='value''" + // Options are of course optional except for requiring a single broker in the broker list. + try + { + URI connection = new URI(fullURL); + + if (connection.getScheme() == null || !(connection.getScheme().equalsIgnoreCase(AMQ_PROTOCOL))) + { + throw new URISyntaxException(fullURL, "Not an AMQP URL"); + } + + if (connection.getHost() == null || connection.getHost().equals("")) + { + String uid = AMQConnectionFactory.getUniqueClientID(); + if (uid == null) + { + URLHelper.parseError(-1, "Client Name not specified", fullURL); + } + else + { + setClientName(uid); + } + + } + else + { + setClientName(connection.getHost()); + } + + String userInfo = connection.getUserInfo(); + + if (userInfo == null) + { + //Fix for Java 1.5 which doesn't parse UserInfo for non http URIs + userInfo = connection.getAuthority(); + + if (userInfo != null) + { + int atIndex = userInfo.indexOf('@'); + + if (atIndex != -1) + { + userInfo = userInfo.substring(0, atIndex); + } + else + { + userInfo = null; + } + } + + } + + if (userInfo == null) + { + URLHelper.parseError(AMQ_PROTOCOL.length() + 3, + "User information not found on url", fullURL); + } + else + { + parseUserInfo(userInfo); + } + String virtualHost = connection.getPath(); + + if (virtualHost != null && (!virtualHost.equals(""))) + { + setVirtualHost(virtualHost); + } + else + { + int authLength = connection.getAuthority().length(); + int start = AMQ_PROTOCOL.length() + 3; + int testIndex = start + authLength; + if (testIndex < fullURL.length() && fullURL.charAt(testIndex) == '?') + { + URLHelper.parseError(start, testIndex - start, "Virtual host found", fullURL); + } + else + { + URLHelper.parseError(-1, "Virtual host not specified", fullURL); + } + + } + + + URLHelper.parseOptions(_options, connection.getQuery()); + + processOptions(); + + //Fragment is #string (not used) + //System.out.println(connection.getFragment()); + + } + catch (URISyntaxException uris) + { + if (uris instanceof URLSyntaxException) + { + throw (URLSyntaxException) uris; + } + + int slash = fullURL.indexOf("\\"); + + if (slash == -1) + { + URLHelper.parseError(uris.getIndex(), uris.getReason(), uris.getInput()); + } + else + { + if (slash != 0 && fullURL.charAt(slash - 1) == ':') + { + URLHelper.parseError(slash - 2, fullURL.indexOf('?') - slash + 2, "Virtual host looks like a windows path, forward slash not allowed in URL", fullURL); + } + else + { + URLHelper.parseError(slash, "Forward slash not allowed in URL", fullURL); + } + } + + } + } + + private void parseUserInfo(String userinfo) throws URLSyntaxException + { + //user info = user:pass + + int colonIndex = userinfo.indexOf(':'); + + if (colonIndex == -1) + { + URLHelper.parseError(AMQ_PROTOCOL.length() + 3, userinfo.length(), + "Null password in user information not allowed.", _url); + } + else + { + setUsername(userinfo.substring(0, colonIndex)); + setPassword(userinfo.substring(colonIndex + 1)); + } + + } + + private void processOptions() throws URLSyntaxException + { + if (_options.containsKey(OPTIONS_BROKERLIST)) + { + String brokerlist = _options.get(OPTIONS_BROKERLIST); + + //brokerlist tcp://host:port?option='value',option='value';vm://:3/virtualpath?option='value' + StringTokenizer st = new StringTokenizer(brokerlist, "" + URLHelper.BROKER_SEPARATOR); + + while (st.hasMoreTokens()) + { + String broker = st.nextToken(); + + _brokers.add(new AMQBrokerDetails(broker)); + } + + _options.remove(OPTIONS_BROKERLIST); + } + + if (_options.containsKey(OPTIONS_FAILOVER)) + { + String failover = _options.get(OPTIONS_FAILOVER); + + // failover='method?option='value',option='value'' + + int methodIndex = failover.indexOf('?'); + + if (methodIndex > -1) + { + _failoverMethod = failover.substring(0, methodIndex); + URLHelper.parseOptions(_failoverOptions, failover.substring(methodIndex + 1)); + } + else + { + _failoverMethod = failover; + } + + _options.remove(OPTIONS_FAILOVER); + } + } + + public String getURL() + { + return _url; + } + + public String getFailoverMethod() + { + return _failoverMethod; + } + + public String getFailoverOption(String key) + { + return _failoverOptions.get(key); + } + + public void setFailoverOption(String key, String value) + { + _failoverOptions.put(key, value); + } + + public int getBrokerCount() + { + return _brokers.size(); + } + + public BrokerDetails getBrokerDetails(int index) + { + if (index < _brokers.size()) + { + return _brokers.get(index); + } + else + { + return null; + } + } + + public void addBrokerDetails(BrokerDetails broker) + { + if (!(_brokers.contains(broker))) + { + _brokers.add(broker); + } + } + + public List<BrokerDetails> getAllBrokerDetails() + { + return _brokers; + } + + public String getClientName() + { + return _clientName; + } + + public void setClientName(String clientName) + { + _clientName = clientName; + } + + public String getUsername() + { + return _username; + } + + public void setUsername(String username) + { + _username = username; + } + + public String getPassword() + { + return _password; + } + + public void setPassword(String password) + { + _password = password; + } + + public String getVirtualHost() + { + return _virtualHost; + } + + public void setVirtualHost(String virtuaHost) + { + _virtualHost = virtuaHost; + } + + public String getOption(String key) + { + return _options.get(key); + } + + public void setOption(String key, String value) + { + _options.put(key, value); + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + + sb.append(AMQ_PROTOCOL); + sb.append("://"); + + if (_username != null) + { + sb.append(_username); + + if (_password != null) + { + sb.append(':'); + sb.append(_password); + } + + sb.append('@'); + } + + sb.append(_clientName); + + sb.append(_virtualHost); + + sb.append(optionsToString()); + + return sb.toString(); + } + + private String optionsToString() + { + StringBuffer sb = new StringBuffer(); + + sb.append("?" + OPTIONS_BROKERLIST + "='"); + + for (BrokerDetails service : _brokers) + { + sb.append(service.toString()); + sb.append(';'); + } + + sb.deleteCharAt(sb.length() - 1); + sb.append("'"); + + if (_failoverMethod != null) + { + sb.append(URLHelper.DEFAULT_OPTION_SEPERATOR); + sb.append(OPTIONS_FAILOVER + "='"); + sb.append(_failoverMethod); + sb.append(URLHelper.printOptions(_failoverOptions)); + sb.append("'"); + } + + return sb.toString(); + } + + + public static void main(String[] args) throws URLSyntaxException + { + + String url2 = "amqp://ritchiem:bob@temp?brokerlist='tcp://localhost:5672;jcp://fancyserver:3000/',failover='roundrobin'"; + //"amqp://user:pass@clientid/virtualhost?brokerlist='tcp://host:1?option1=\'value\',option2=\'value\';vm://:3?option1=\'value\'',failover='method?option1=\'value\',option2='value''"; + + ConnectionURL connectionurl2 = new AMQConnectionURL(url2); + + System.out.println(url2); + System.out.println(connectionurl2); + + } + +} diff --git a/java/client/src/org/apache/qpid/client/AMQDestination.java b/java/client/src/org/apache/qpid/client/AMQDestination.java new file mode 100644 index 0000000000..5deb974c5b --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQDestination.java @@ -0,0 +1,282 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.url.BindingURL; +import org.apache.qpid.url.AMQBindingURL; +import org.apache.qpid.url.URLSyntaxException; +import org.apache.qpid.url.URLHelper; +import org.apache.qpid.exchange.ExchangeDefaults; + +import javax.naming.Reference; +import javax.naming.NamingException; +import javax.naming.StringRefAddr; +import javax.naming.Referenceable; +import javax.jms.Destination; + + +public abstract class AMQDestination implements Destination, Referenceable +{ + protected final String _exchangeName; + + protected final String _exchangeClass; + + protected final String _destinationName; + + protected boolean _isDurable; + + protected final boolean _isExclusive; + + protected final boolean _isAutoDelete; + + protected String _queueName; + + protected AMQDestination(String url) throws URLSyntaxException + { + this(new AMQBindingURL(url)); + } + + protected AMQDestination(BindingURL binding) + { + _exchangeName = binding.getExchangeName(); + _exchangeClass = binding.getExchangeClass(); + _destinationName = binding.getDestinationName(); + + _isExclusive = Boolean.parseBoolean(binding.getOption(BindingURL.OPTION_EXCLUSIVE)); + _isAutoDelete = Boolean.parseBoolean(binding.getOption(BindingURL.OPTION_AUTODELETE)); + _isDurable = Boolean.parseBoolean(binding.getOption(BindingURL.OPTION_DURABLE)); + _queueName = binding.getQueueName(); + } + + protected AMQDestination(String exchangeName, String exchangeClass, String destinationName, String queueName) + { + this(exchangeName, exchangeClass, destinationName, false, false, queueName); + } + + protected AMQDestination(String exchangeName, String exchangeClass, String destinationName) + { + this(exchangeName, exchangeClass, destinationName, false, false, null); + } + + protected AMQDestination(String exchangeName, String exchangeClass, String destinationName, boolean isExclusive, + boolean isAutoDelete, String queueName) + { + if (destinationName == null) + { + throw new IllegalArgumentException("Destination name must not be null"); + } + if (exchangeName == null) + { + throw new IllegalArgumentException("Exchange name must not be null"); + } + if (exchangeClass == null) + { + throw new IllegalArgumentException("Exchange class must not be null"); + } + _exchangeName = exchangeName; + _exchangeClass = exchangeClass; + _destinationName = destinationName; + _isExclusive = isExclusive; + _isAutoDelete = isAutoDelete; + _queueName = queueName; + } + + public abstract String getEncodedName(); + + public boolean isDurable() + { + return _isDurable; + } + + public String getExchangeName() + { + return _exchangeName; + } + + public String getExchangeClass() + { + return _exchangeClass; + } + + public boolean isTopic() + { + return ExchangeDefaults.TOPIC_EXCHANGE_NAME.equals(_exchangeName); + } + + public boolean isQueue() + { + return ExchangeDefaults.DIRECT_EXCHANGE_NAME.equals(_exchangeName); + } + + public String getDestinationName() + { + return _destinationName; + } + + public String getQueueName() + { + return _queueName; + } + + public void setQueueName(String queueName) + { + _queueName = queueName; + } + + public abstract String getRoutingKey(); + + public boolean isExclusive() + { + return _isExclusive; + } + + public boolean isAutoDelete() + { + return _isAutoDelete; + } + + public abstract boolean isNameRequired(); + + public String toString() + { + return toURL(); + + /* + return "Destination: " + _destinationName + ", " + + "Queue Name: " + _queueName + ", Exchange: " + _exchangeName + + ", Exchange class: " + _exchangeClass + ", Exclusive: " + _isExclusive + + ", AutoDelete: " + _isAutoDelete + ", Routing Key: " + getRoutingKey(); + */ + } + + public String toURL() + { + StringBuffer sb = new StringBuffer(); + + sb.append(_exchangeClass); + sb.append("://"); + sb.append(_exchangeName); + + sb.append("/"); + + if (_destinationName != null) + { + sb.append(_destinationName); + } + + sb.append("/"); + + if (_queueName != null) + { + sb.append(_queueName); + } + + sb.append("?"); + + if (_isDurable) + { + sb.append(BindingURL.OPTION_DURABLE); + sb.append("='true'"); + sb.append(URLHelper.DEFAULT_OPTION_SEPERATOR); + } + + if (_isExclusive) + { + sb.append(BindingURL.OPTION_EXCLUSIVE); + sb.append("='true'"); + sb.append(URLHelper.DEFAULT_OPTION_SEPERATOR); + } + + if (_isAutoDelete) + { + sb.append(BindingURL.OPTION_AUTODELETE); + sb.append("='true'"); + sb.append(URLHelper.DEFAULT_OPTION_SEPERATOR); + } + + //remove the last char '?' if there is no options , ',' if there are. + sb.deleteCharAt(sb.length() - 1); + + return sb.toString(); + } + + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + + final AMQDestination that = (AMQDestination) o; + + if (!_destinationName.equals(that._destinationName)) + { + return false; + } + if (!_exchangeClass.equals(that._exchangeClass)) + { + return false; + } + if (!_exchangeName.equals(that._exchangeName)) + { + return false; + } + if ((_queueName == null && that._queueName != null) || + (_queueName != null && !_queueName.equals(that._queueName))) + { + return false; + } + if (_isExclusive != that._isExclusive) + { + return false; + } + if (_isAutoDelete != that._isAutoDelete) + { + return false; + } + return true; + } + + public int hashCode() + { + int result; + result = _exchangeName.hashCode(); + result = 29 * result + _exchangeClass.hashCode(); + result = 29 * result + _destinationName.hashCode(); + if (_queueName != null) + { + result = 29 * result + _queueName.hashCode(); + } + result = result * (_isExclusive ? 13 : 7); + result = result * (_isAutoDelete ? 13 : 7); + return result; + } + + public Reference getReference() throws NamingException + { + return new Reference( + this.getClass().getName(), + new StringRefAddr(this.getClass().getName(), toURL()), + AMQConnectionFactory.class.getName(), + null); // factory location + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQHeadersExchange.java b/java/client/src/org/apache/qpid/client/AMQHeadersExchange.java new file mode 100644 index 0000000000..5b8de7a13f --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQHeadersExchange.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.exchange.ExchangeDefaults; + +/** + * A destination backed by a headers exchange + */ +public class AMQHeadersExchange extends AMQDestination +{ + public AMQHeadersExchange(String queueName) + { + super(queueName, ExchangeDefaults.HEADERS_EXCHANGE_CLASS, queueName, true, true, null); + } + + public String getEncodedName() + { + return getDestinationName(); + } + + public String getRoutingKey() + { + return getDestinationName(); + } + + public boolean isNameRequired() + { + //Not sure what the best approach is here, probably to treat this like a topic + //and allow server to generate names. As it is AMQ specific it doesn't need to + //fit the JMS API expectations so this is not as yet critical. + return false; + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQNoConsumersException.java b/java/client/src/org/apache/qpid/client/AMQNoConsumersException.java new file mode 100644 index 0000000000..19d4da0b8c --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQNoConsumersException.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.AMQUndeliveredException; +import org.apache.qpid.protocol.AMQConstant; + + +public class AMQNoConsumersException extends AMQUndeliveredException +{ + public AMQNoConsumersException(String msg, Object bounced) + { + super(AMQConstant.NO_CONSUMERS.getCode(), msg, bounced); + } + + +} + + diff --git a/java/client/src/org/apache/qpid/client/AMQNoRouteException.java b/java/client/src/org/apache/qpid/client/AMQNoRouteException.java new file mode 100644 index 0000000000..ad885bcbb3 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQNoRouteException.java @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.AMQUndeliveredException; +import org.apache.qpid.protocol.AMQConstant; + + +public class AMQNoRouteException extends AMQUndeliveredException +{ + public AMQNoRouteException(String msg, Object bounced) + { + super(AMQConstant.NO_ROUTE.getCode(), msg, bounced); + } + + +} + + diff --git a/java/client/src/org/apache/qpid/client/AMQQueue.java b/java/client/src/org/apache/qpid/client/AMQQueue.java new file mode 100644 index 0000000000..0a8e1bdd7c --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQQueue.java @@ -0,0 +1,91 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.url.BindingURL; +import org.apache.qpid.exchange.ExchangeDefaults; + +import javax.jms.Queue; + +public class AMQQueue extends AMQDestination implements Queue +{ + + /** + * Create a reference to a non temporary queue using a BindingURL object. + * Note this does not actually imply the queue exists. + * @param binding a BindingURL object + */ + public AMQQueue(BindingURL binding) + { + super(binding); + } + + /** + * Create a reference to a non temporary queue. Note this does not actually imply the queue exists. + * @param name the name of the queue + */ + public AMQQueue(String name) + { + this(name, false); + } + + /** + * Create a queue with a specified name. + * + * @param name the destination name (used in the routing key) + * @param temporary if true the broker will generate a queue name, also if true then the queue is autodeleted + * and exclusive + */ + public AMQQueue(String name, boolean temporary) + { + // queue name is set to null indicating that the broker assigns a name in the case of temporary queues + // temporary queues are typically used as response queues + this(name, temporary?null:name, temporary, temporary); + _isDurable = !temporary; + } + + /** + * Create a reference to a queue. Note this does not actually imply the queue exists. + * @param destinationName the queue name + * @param queueName the queue name + * @param exclusive true if the queue should only permit a single consumer + * @param autoDelete true if the queue should be deleted automatically when the last consumers detaches + */ + public AMQQueue(String destinationName, String queueName, boolean exclusive, boolean autoDelete) + { + super(ExchangeDefaults.DIRECT_EXCHANGE_NAME, ExchangeDefaults.DIRECT_EXCHANGE_CLASS, destinationName, exclusive, + autoDelete, queueName); + } + + public String getEncodedName() + { + return 'Q' + getQueueName(); + } + + public String getRoutingKey() + { + return getQueueName(); + } + + public boolean isNameRequired() + { + //If the name is null, we require one to be generated by the client so that it will# + //remain valid if we failover (see BLZ-24) + return getQueueName() == null; + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQSession.java b/java/client/src/org/apache/qpid/client/AMQSession.java new file mode 100644 index 0000000000..6fd4e9cbef --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQSession.java @@ -0,0 +1,1149 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.AMQUndeliveredException; +import org.apache.qpid.client.message.AbstractJMSMessage; +import org.apache.qpid.client.message.MessageFactoryRegistry; +import org.apache.qpid.client.message.UnprocessedMessage; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.client.failover.FailoverSupport; +import org.apache.qpid.client.util.FlowControllingBlockingQueue; +import org.apache.qpid.framing.*; +import org.apache.qpid.jms.Session; +import org.apache.qpid.protocol.AMQConstant; +import org.apache.qpid.url.AMQBindingURL; +import org.apache.qpid.url.URLSyntaxException; + +import javax.jms.*; +import javax.jms.IllegalStateException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.text.MessageFormat; + +public class AMQSession extends Closeable implements Session, QueueSession, TopicSession +{ + private static final Logger _logger = Logger.getLogger(AMQSession.class); + + public static final int DEFAULT_PREFETCH = 5000; + + private AMQConnection _connection; + + private boolean _transacted; + + private int _acknowledgeMode; + + private int _channelId; + + private int _defaultPrefetch = DEFAULT_PREFETCH; + + /** + * Used in the consume method. We generate the consume tag on the client so that we can use the nowait + * feature. + */ + private int _nextTag = 1; + + /** + * This queue is bounded and is used to store messages before being dispatched to the consumer + */ + private final FlowControllingBlockingQueue _queue; + + private Dispatcher _dispatcher; + + private MessageFactoryRegistry _messageFactoryRegistry; + + /** + * Set of all producers created by this session + */ + private Map _producers = new ConcurrentHashMap(); + + /** + * Maps from consumer tag (String) to JMSMessageConsumer instance + */ + private Map _consumers = new ConcurrentHashMap(); + + /** + * Default value for immediate flag used by producers created by this session is false, i.e. a consumer does not + * need to be attached to a queue + */ + protected static final boolean DEFAULT_IMMEDIATE = false; + + /** + * Default value for mandatory flag used by producers created by this sessio is true, i.e. server will not silently + * drop messages where no queue is connected to the exchange for the message + */ + protected static final boolean DEFAULT_MANDATORY = true; + + /** + * The counter of the next producer id. This id is generated by the session and used only to allow the + * producer to identify itself to the session when deregistering itself. + * + * Access to this id does not require to be synchronized since according to the JMS specification only one + * thread of control is allowed to create producers for any given session instance. + */ + private long _nextProducerId; + + /** + * Track the 'stopped' state of the dispatcher, a session starts in the stopped state. + */ + private volatile AtomicBoolean _stopped = new AtomicBoolean(true); + + /** + * Responsible for decoding a message fragment and passing it to the appropriate message consumer. + */ + private class Dispatcher extends Thread + { + public Dispatcher() + { + super("Dispatcher-Channel-" + _channelId); + } + + public void run() + { + UnprocessedMessage message; + _stopped.set(false); + try + { + while (!_stopped.get() && (message = (UnprocessedMessage)_queue.take()) != null) + { + dispatchMessage(message); + } + } + catch(InterruptedException e) + { + ; + } + + _logger.info("Dispatcher thread terminating for channel " + _channelId); + } + + private void dispatchMessage(UnprocessedMessage message) + { + if (message.deliverBody != null) + { + final BasicMessageConsumer consumer = (BasicMessageConsumer) _consumers.get(message.deliverBody.consumerTag); + + if (consumer == null) + { + _logger.warn("Received a message from queue " + message.deliverBody.consumerTag + " without a handler - ignoring..."); + } + else + { + consumer.notifyMessage(message, _channelId); + } + } + else + { + try + { + // Bounced message is processed here, away from the mina thread + AbstractJMSMessage bouncedMessage = _messageFactoryRegistry.createMessage(0, + false, + message.contentHeader, + message.bodies); + + int errorCode = message.bounceBody.replyCode; + String reason = message.bounceBody.replyText; + _logger.debug("Message returned with error code " + errorCode + " (" + reason + ")"); + + //Todo should this be moved to an exception handler of sorts. Somewhere errors are converted to correct execeptions. + if (errorCode == AMQConstant.NO_CONSUMERS.getCode()) + { + _connection.exceptionReceived(new AMQNoConsumersException("Error: " + reason, bouncedMessage)); + } + else + { + if (errorCode == AMQConstant.NO_ROUTE.getCode()) + { + _connection.exceptionReceived(new AMQNoRouteException("Error: " + reason, bouncedMessage)); + } + else + { + _connection.exceptionReceived(new AMQUndeliveredException(errorCode, "Error: " + reason, bouncedMessage)); + } + } + } + catch (Exception e) + { + _logger.error("Caught exception trying to raise undelivered message exception (dump follows) - ignoring...", e); + } + } + } + + public void stopDispatcher() + { + _stopped.set(true); + interrupt(); + } + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode, + MessageFactoryRegistry messageFactoryRegistry) + { + this(con, channelId, transacted, acknowledgeMode, messageFactoryRegistry, DEFAULT_PREFETCH); + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode, + MessageFactoryRegistry messageFactoryRegistry, int defaultPrefetch) + { + _connection = con; + _transacted = transacted; + if (transacted) + { + _acknowledgeMode = javax.jms.Session.SESSION_TRANSACTED; + } + else + { + _acknowledgeMode = acknowledgeMode; + } + _channelId = channelId; + _messageFactoryRegistry = messageFactoryRegistry; + _defaultPrefetch = defaultPrefetch; + _queue = new FlowControllingBlockingQueue(_defaultPrefetch, + new FlowControllingBlockingQueue.ThresholdListener() + { + public void aboveThreshold(int currentValue) + { + if(_acknowledgeMode == NO_ACKNOWLEDGE) + { + _logger.warn("Above threshold so suspending channel. Current value is " + currentValue); + suspendChannel(); + } + } + + public void underThreshold(int currentValue) + { + if(_acknowledgeMode == NO_ACKNOWLEDGE) + { + _logger.warn("Below threshold so unsuspending channel. Current value is " + currentValue); + unsuspendChannel(); + } + } + }); + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode) + { + this(con, channelId, transacted, acknowledgeMode, MessageFactoryRegistry.newDefaultRegistry()); + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode, int defaultPrefetch) + { + this(con, channelId, transacted, acknowledgeMode, MessageFactoryRegistry.newDefaultRegistry(), defaultPrefetch); + } + + AMQConnection getAMQConnection() + { + return _connection; + } + + public BytesMessage createBytesMessage() throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + try + { + return (BytesMessage) _messageFactoryRegistry.createMessage("application/octet-stream"); + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public MapMessage createMapMessage() throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + try + { + return (MapMessage) _messageFactoryRegistry.createMessage("jms/map-message"); + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public javax.jms.Message createMessage() throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + try + { + return (BytesMessage) _messageFactoryRegistry.createMessage("application/octet-stream"); + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public ObjectMessage createObjectMessage() throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + try + { + return (ObjectMessage) _messageFactoryRegistry.createMessage("application/java-object-stream"); + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public ObjectMessage createObjectMessage(Serializable object) throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + try + { + ObjectMessage msg = (ObjectMessage) _messageFactoryRegistry.createMessage("application/java-object-stream"); + msg.setObject(object); + return msg; + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public StreamMessage createStreamMessage() throws JMSException + { + checkNotClosed(); + throw new UnsupportedOperationException("Stream messages not supported"); + } + + public TextMessage createTextMessage() throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + + try + { + return (TextMessage) _messageFactoryRegistry.createMessage("text/plain"); + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public TextMessage createTextMessage(String text) throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + checkNotClosed(); + try + { + TextMessage msg = (TextMessage) _messageFactoryRegistry.createMessage("text/plain"); + msg.setText(text); + return msg; + } + catch (AMQException e) + { + throw new JMSException("Unable to create message: " + e); + } + } + } + + public boolean getTransacted() + { + checkNotClosed(); + return _transacted; + } + + public int getAcknowledgeMode() throws JMSException + { + checkNotClosed(); + return _acknowledgeMode; + } + + public void commit() throws JMSException + { + checkTransacted(); + try + { + // Acknowledge up to message last delivered (if any) for each consumer. + //need to send ack for messages delivered to consumers so far + for(Iterator i = _consumers.values().iterator(); i.hasNext();) + { + ((BasicMessageConsumer) i.next()).acknowledgeLastDelivered(); + } + + // Commits outstanding messages sent and outstanding acknowledgements. + _connection.getProtocolHandler().syncWrite(TxCommitBody.createAMQFrame(_channelId), TxCommitOkBody.class); + } + catch (AMQException e) + { + JMSException exception = new JMSException("Failed to commit: " + e.getMessage()); + exception.setLinkedException(e); + throw exception; + } + } + + public void rollback() throws JMSException + { + checkTransacted(); + try + { + _connection.getProtocolHandler().syncWrite( + TxRollbackBody.createAMQFrame(_channelId), TxRollbackOkBody.class); + } + catch (AMQException e) + { + throw (JMSException) (new JMSException("Failed to rollback: " + e).initCause(e)); + } + } + + public void close() throws JMSException + { + // We must close down all producers and consumers in an orderly fashion. This is the only method + // that can be called from a different thread of control from the one controlling the session + synchronized (_connection.getFailoverMutex()) + { + _closed.set(true); + + // we pass null since this is not an error case + closeProducersAndConsumers(null); + + try + { + _connection.getProtocolHandler().closeSession(this); + final AMQFrame frame = ChannelCloseBody.createAMQFrame( + getChannelId(), AMQConstant.REPLY_SUCCESS.getCode(), "JMS client closing channel", 0, 0); + _connection.getProtocolHandler().syncWrite(frame, ChannelCloseOkBody.class); + // When control resumes at this point, a reply will have been received that + // indicates the broker has closed the channel successfully + + } + catch (AMQException e) + { + throw new JMSException("Error closing session: " + e); + } + finally + { + _connection.deregisterSession(_channelId); + } + } + } + + /** + * Close all producers or consumers. This is called either in the error case or when closing the session normally. + * @param amqe the exception, may be null to indicate no error has occurred + */ + private void closeProducersAndConsumers(AMQException amqe) + { + try + { + closeProducers(); + } + catch (JMSException e) + { + _logger.error("Error closing session: " + e, e); + } + try + { + closeConsumers(amqe); + } + catch (JMSException e) + { + _logger.error("Error closing session: " + e, e); + } + } + + /** + * Called when the server initiates the closure of the session + * unilaterally. + * @param e the exception that caused this session to be closed. Null causes the + */ + public void closed(Throwable e) + { + synchronized (_connection.getFailoverMutex()) + { + // An AMQException has an error code and message already and will be passed in when closure occurs as a + // result of a channel close request + _closed.set(true); + AMQException amqe; + if (e instanceof AMQException) + { + amqe = (AMQException) e; + } + else + { + amqe = new AMQException(_logger, "Closing session forcibly", e); + } + _connection.deregisterSession(_channelId); + closeProducersAndConsumers(amqe); + } + } + + /** + * Called to mark the session as being closed. Useful when the session needs to be made invalid, e.g. after + * failover when the client has veoted resubscription. + * + * The caller of this method must already hold the failover mutex. + */ + void markClosed() + { + _closed.set(true); + _connection.deregisterSession(_channelId); + markClosedProducersAndConsumers(); + } + + private void markClosedProducersAndConsumers() + { + try + { + // no need for a markClosed* method in this case since there is no protocol traffic closing a producer + closeProducers(); + } + catch (JMSException e) + { + _logger.error("Error closing session: " + e, e); + } + try + { + markClosedConsumers(); + } + catch (JMSException e) + { + _logger.error("Error closing session: " + e, e); + } + } + + /** + * Called to close message producers cleanly. This may or may <b>not</b> be as a result of an error. There is + * currently no way of propagating errors to message producers (this is a JMS limitation). + */ + private void closeProducers() throws JMSException + { + // we need to clone the list of producers since the close() method updates the _producers collection + // which would result in a concurrent modification exception + final ArrayList clonedProducers = new ArrayList(_producers.values()); + + final Iterator it = clonedProducers.iterator(); + while (it.hasNext()) + { + final BasicMessageProducer prod = (BasicMessageProducer) it.next(); + prod.close(); + } + // at this point the _producers map is empty + } + + /** + * Called to close message consumers cleanly. This may or may <b>not</b> be as a result of an error. + * @param error not null if this is a result of an error occurring at the connection level + */ + private void closeConsumers(Throwable error) throws JMSException + { + if (_dispatcher != null) + { + _dispatcher.stopDispatcher(); + } + // we need to clone the list of consumers since the close() method updates the _consumers collection + // which would result in a concurrent modification exception + final ArrayList clonedConsumers = new ArrayList(_consumers.values()); + + final Iterator it = clonedConsumers.iterator(); + while (it.hasNext()) + { + final BasicMessageConsumer con = (BasicMessageConsumer) it.next(); + if (error != null) + { + con.notifyError(error); + } + else + { + con.close(); + } + } + // at this point the _consumers map will be empty + } + + private void markClosedConsumers() throws JMSException + { + if (_dispatcher != null) + { + _dispatcher.stopDispatcher(); + } + // we need to clone the list of consumers since the close() method updates the _consumers collection + // which would result in a concurrent modification exception + final ArrayList clonedConsumers = new ArrayList(_consumers.values()); + + final Iterator it = clonedConsumers.iterator(); + while (it.hasNext()) + { + final BasicMessageConsumer con = (BasicMessageConsumer) it.next(); + con.markClosed(); + } + // at this point the _consumers map will be empty + } + + /** + * Asks the broker to resend all unacknowledged messages for the session. + * @throws JMSException + */ + public void recover() throws JMSException + { + checkNotClosed(); + checkNotTransacted(); // throws IllegalStateException if a transacted session + + _connection.getProtocolHandler().writeFrame(BasicRecoverBody.createAMQFrame(_channelId, false)); + } + + public MessageListener getMessageListener() throws JMSException + { + checkNotClosed(); + throw new java.lang.UnsupportedOperationException("MessageListener interface not supported"); + } + + public void setMessageListener(MessageListener listener) throws JMSException + { + checkNotClosed(); + throw new java.lang.UnsupportedOperationException("MessageListener interface not supported"); + } + + public void run() + { + throw new java.lang.UnsupportedOperationException(); + } + + public MessageProducer createProducer(Destination destination, boolean mandatory, + boolean immediate, boolean waitUntilSent) + throws JMSException + { + return createProducerImpl(destination, mandatory, immediate, waitUntilSent); + } + + public MessageProducer createProducer(Destination destination, boolean mandatory, boolean immediate) + throws JMSException + { + return createProducerImpl(destination, mandatory, immediate); + } + + public MessageProducer createProducer(Destination destination, boolean immediate) + throws JMSException + { + return createProducerImpl(destination, DEFAULT_MANDATORY, immediate); + } + + public MessageProducer createProducer(Destination destination) throws JMSException + { + return createProducerImpl(destination, DEFAULT_MANDATORY, DEFAULT_IMMEDIATE); + } + + private org.apache.qpid.jms.MessageProducer createProducerImpl(Destination destination, boolean mandatory, + boolean immediate) + throws JMSException + { + return createProducerImpl(destination, mandatory, immediate, false); + } + + private org.apache.qpid.jms.MessageProducer createProducerImpl(final Destination destination, final boolean mandatory, + final boolean immediate, final boolean waitUntilSent) + throws JMSException + { + return (org.apache.qpid.jms.MessageProducer) new FailoverSupport() + { + public Object operation() + { + checkNotClosed(); + + return new BasicMessageProducer(_connection, (AMQDestination)destination, _transacted, _channelId, + AMQSession.this, _connection.getProtocolHandler(), + getNextProducerId(), immediate, mandatory, waitUntilSent); + } + }.execute(_connection); + } + + public MessageConsumer createConsumer(Destination destination) throws JMSException + { + return createConsumer(destination, _defaultPrefetch, false, false, null); + } + + public MessageConsumer createConsumer(Destination destination, String messageSelector) throws JMSException + { + return createConsumer(destination, _defaultPrefetch, false, false, messageSelector); + } + + public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean noLocal) + throws JMSException + { + return createConsumer(destination, _defaultPrefetch, noLocal, false, messageSelector); + } + + public MessageConsumer createConsumer(Destination destination, + int prefetch, + boolean noLocal, + boolean exclusive, + String selector) throws JMSException + { + return createConsumer(destination, prefetch, noLocal, exclusive, selector, null); + } + + public MessageConsumer createConsumer(Destination destination, + int prefetch, + boolean noLocal, + boolean exclusive, + String selector, + FieldTable rawSelector) throws JMSException + { + return createConsumerImpl(destination, prefetch, noLocal, exclusive, + selector, rawSelector); + } + + protected MessageConsumer createConsumerImpl(final Destination destination, + final int prefetch, + final boolean noLocal, + final boolean exclusive, + final String selector, + final FieldTable rawSelector) throws JMSException + { + return (org.apache.qpid.jms.MessageConsumer) new FailoverSupport() + { + public Object operation() throws JMSException + { + checkNotClosed(); + + AMQDestination amqd = (AMQDestination)destination; + + final AMQProtocolHandler protocolHandler = _connection.getProtocolHandler(); + // TODO: construct the rawSelector from the selector string if rawSelector == null + final FieldTable ft = new FieldTable(); + //if (rawSelector != null) + // ft.put("headers", rawSelector.getDataAsBytes()); + if (rawSelector != null) + { + ft.putAll(rawSelector); + } + BasicMessageConsumer consumer = new BasicMessageConsumer(_channelId, _connection, amqd, selector, noLocal, + _messageFactoryRegistry, AMQSession.this, + protocolHandler, ft, prefetch, exclusive, + _acknowledgeMode); + + try + { + registerConsumer(consumer); + } + catch (AMQException e) + { + JMSException ex = new JMSException("Error registering consumer: " + e); + ex.setLinkedException(e); + throw ex; + } + + return consumer; + } + }.execute(_connection); + } + + public void declareExchange(String name, String type) + { + declareExchange(name, type, _connection.getProtocolHandler()); + } + + private void declareExchange(AMQDestination amqd, AMQProtocolHandler protocolHandler) + { + declareExchange(amqd.getExchangeName(), amqd.getExchangeClass(), protocolHandler); + } + + private void declareExchange(String name, String type, AMQProtocolHandler protocolHandler) + { + AMQFrame exchangeDeclare = ExchangeDeclareBody.createAMQFrame(_channelId, 0, name, type, false, false, false, false, true, null); + protocolHandler.writeFrame(exchangeDeclare); + } + + /** + * Declare the queue. + * @param amqd + * @param protocolHandler + * @return the queue name. This is useful where the broker is generating a queue name on behalf of the client. + * @throws AMQException + */ + private String declareQueue(AMQDestination amqd, AMQProtocolHandler protocolHandler) throws AMQException + { + // For queues (but not topics) we generate the name in the client rather than the + // server. This allows the name to be reused on failover if required. In general, + // the destination indicates whether it wants a name generated or not. + if(amqd.isNameRequired()) + { + amqd.setQueueName(protocolHandler.generateQueueName()); + } + + AMQFrame queueDeclare = QueueDeclareBody.createAMQFrame(_channelId, 0, amqd.getQueueName(), + false, amqd.isDurable(), amqd.isExclusive(), + amqd.isAutoDelete(), true, null); + + protocolHandler.writeFrame(queueDeclare); + return amqd.getQueueName(); + } + + private void bindQueue(AMQDestination amqd, String queueName, AMQProtocolHandler protocolHandler, FieldTable ft) throws AMQException + { + AMQFrame queueBind = QueueBindBody.createAMQFrame(_channelId, 0, + queueName, amqd.getExchangeName(), + amqd.getRoutingKey(), true, ft); + + protocolHandler.writeFrame(queueBind); + } + + /** + * Register to consume from the queue. + * @param queueName + * @return the consumer tag generated by the broker + */ + private String consumeFromQueue(String queueName, AMQProtocolHandler protocolHandler, int prefetch, + boolean noLocal, boolean exclusive, int acknowledgeMode) throws AMQException + { + //need to generate a consumer tag on the client so we can exploit the nowait flag + String tag = Integer.toString(_nextTag++); + + AMQFrame jmsConsume = BasicConsumeBody.createAMQFrame(_channelId, 0, + queueName, tag, noLocal, + acknowledgeMode == Session.NO_ACKNOWLEDGE, + exclusive, true); + + protocolHandler.writeFrame(jmsConsume); + return tag; + } + + public Queue createQueue(String queueName) throws JMSException + { + if (queueName.indexOf('/') == -1) + { + return new AMQQueue(queueName); + } + else + { + try{ + return new AMQQueue(new AMQBindingURL(queueName)); + }catch(URLSyntaxException urlse) + { + JMSException jmse = new JMSException(urlse.getReason()); + jmse.setLinkedException(urlse); + + throw jmse; + } + } + } + + public QueueReceiver createReceiver(Queue queue) throws JMSException + { + return (QueueReceiver) createConsumer(queue); + } + + public QueueReceiver createReceiver(Queue queue, String messageSelector) throws JMSException + { + return (QueueReceiver) createConsumer(queue, messageSelector); + } + + public QueueSender createSender(Queue queue) throws JMSException + { + return (QueueSender) createProducer(queue); + } + + public Topic createTopic(String topicName) throws JMSException + { + if (topicName.indexOf('/') == -1) + { + return new AMQTopic(topicName); + } + else + { + try{ + return new AMQTopic(new AMQBindingURL(topicName)); + } + catch (URLSyntaxException urlse) + { + JMSException jmse = new JMSException(urlse.getReason()); + jmse.setLinkedException(urlse); + + throw jmse; + } + } + } + + public TopicSubscriber createSubscriber(Topic topic) throws JMSException + { + return (TopicSubscriber) createConsumer(topic); + } + + public TopicSubscriber createSubscriber(Topic topic, String messageSelector, boolean noLocal) throws JMSException + { + return (TopicSubscriber) createConsumer(topic, messageSelector, noLocal); + } + + /** + * Note, currently this does not handle reuse of the same name with different topics correctly. + * If a name is reused in creating a new subscriber with a different topic/selecto or no-local + * flag then the subcriber will receive messages matching the old subscription AND the new one. + * The spec states that the new one should replace the old one. + * TODO: fix it. + */ + public TopicSubscriber createDurableSubscriber(Topic topic, String name) throws JMSException + { + AMQTopic dest = new AMQTopic((AMQTopic) topic, _connection.getClientID(), name); + return new TopicSubscriberAdaptor(dest, (BasicMessageConsumer) createConsumer(dest)); + } + + /** + * Note, currently this does not handle reuse of the same name with different topics correctly. + */ + public TopicSubscriber createDurableSubscriber(Topic topic, String name, String messageSelector, boolean noLocal) + throws JMSException + { + AMQTopic dest = new AMQTopic((AMQTopic) topic, _connection.getClientID(), name); + BasicMessageConsumer consumer = (BasicMessageConsumer) createConsumer(dest, messageSelector, noLocal); + return new TopicSubscriberAdaptor(dest, consumer); + } + + public TopicPublisher createPublisher(Topic topic) throws JMSException + { + return (TopicPublisher) createProducer(topic); + } + + public QueueBrowser createBrowser(Queue queue) throws JMSException + { + throw new UnsupportedOperationException("Queue browsing not supported"); + } + + public QueueBrowser createBrowser(Queue queue, String messageSelector) throws JMSException + { + throw new UnsupportedOperationException("Queue browsing not supported"); + } + + public TemporaryQueue createTemporaryQueue() throws JMSException + { + return new AMQTemporaryQueue(); + } + + public TemporaryTopic createTemporaryTopic() throws JMSException + { + return new AMQTemporaryTopic(); + } + + public void unsubscribe(String name) throws JMSException + { + //send a queue.delete for the subscription + String queue = _connection.getClientID() + ":" + name; + AMQFrame frame = QueueDeleteBody.createAMQFrame(_channelId, 0, queue, false, false, true); + _connection.getProtocolHandler().writeFrame(frame); + } + + private void checkTransacted() throws JMSException + { + if (!getTransacted()) + { + throw new IllegalStateException("Session is not transacted"); + } + } + + private void checkNotTransacted() throws JMSException + { + if (getTransacted()) + { + throw new IllegalStateException("Session is transacted"); + } + } + + /** + * Invoked by the MINA IO thread (indirectly) when a message is received from the transport. + * Puts the message onto the queue read by the dispatcher. + * + * @param message the message that has been received + */ + public void messageReceived(UnprocessedMessage message) + { + if (_logger.isDebugEnabled()) + { + _logger.debug("Message received in session with channel id " + _channelId); + } + + _queue.add(message); + } + + /** + * Acknowledge a message or several messages. This method can be called via AbstractJMSMessage or from + * a BasicConsumer. The former where the mode is CLIENT_ACK and the latter where the mode is + * AUTO_ACK or similar. + * @param deliveryTag the tag of the last message to be acknowledged + * @param multiple if true will acknowledge all messages up to and including the one specified by the + * delivery tag + */ + public void acknowledgeMessage(long deliveryTag, boolean multiple) + { + final AMQFrame ackFrame = BasicAckBody.createAMQFrame(_channelId, deliveryTag, multiple); + if (_logger.isDebugEnabled()) + { + _logger.debug("Sending ack for delivery tag " + deliveryTag + " on channel " + _channelId); + } + _connection.getProtocolHandler().writeFrame(ackFrame); + } + + public int getDefaultPrefetch() + { + return _defaultPrefetch; + } + + public int getChannelId() + { + return _channelId; + } + + void start() + { + if(_dispatcher != null) + { + //then we stopped this and are restarting, so signal server to resume delivery + unsuspendChannel(); + } + _dispatcher = new Dispatcher(); + _dispatcher.setDaemon(true); + _dispatcher.start(); + } + + void stop() + { + //stop the server delivering messages to this session + suspendChannel(); + + //stop the dispatcher thread + _stopped.set(true); + } + + boolean isStopped() + { + return _stopped.get(); + } + + /** + * Callers must hold the failover mutex before calling this method. + * @param consumer + * @throws AMQException + */ + void registerConsumer(BasicMessageConsumer consumer) throws AMQException + { + AMQDestination amqd = consumer.getDestination(); + + AMQProtocolHandler protocolHandler = _connection.getProtocolHandler(); + + declareExchange(amqd, protocolHandler); + + String queueName = declareQueue(amqd, protocolHandler); + + bindQueue(amqd, queueName, protocolHandler, consumer.getRawSelectorFieldTable()); + + String consumerTag = consumeFromQueue(queueName, protocolHandler, consumer.getPrefetch(), consumer.isNoLocal(), + consumer.isExclusive(), consumer.getAcknowledgeMode()); + + consumer.setConsumerTag(consumerTag); + _consumers.put(consumerTag, consumer); + } + + /** + * Called by the MessageConsumer when closing, to deregister the consumer from the + * map from consumerTag to consumer instance. + * @param consumerTag the consumer tag, that was broker-generated + */ + void deregisterConsumer(String consumerTag) + { + _consumers.remove(consumerTag); + } + + private void registerProducer(long producerId, MessageProducer producer) + { + _producers.put(new Long(producerId), producer); + } + + void deregisterProducer(long producerId) + { + _producers.remove(new Long(producerId)); + } + + private long getNextProducerId() + { + return ++_nextProducerId; + } + + /** + * Resubscribes all producers and consumers. This is called when performing failover. + * @throws AMQException + */ + void resubscribe() throws AMQException + { + resubscribeProducers(); + resubscribeConsumers(); + } + + private void resubscribeProducers() throws AMQException + { + ArrayList producers = new ArrayList(_producers.values()); + _logger.info(MessageFormat.format("Resubscribing producers = {0} producers.size={1}", producers, producers.size())); // FIXME: remove + for (Iterator it = producers.iterator(); it.hasNext();) + { + BasicMessageProducer producer = (BasicMessageProducer) it.next(); + producer.resubscribe(); + } + } + + private void resubscribeConsumers() throws AMQException + { + ArrayList consumers = new ArrayList(_consumers.values()); + _consumers.clear(); + + for (Iterator it = consumers.iterator(); it.hasNext();) + { + BasicMessageConsumer consumer = (BasicMessageConsumer) it.next(); + registerConsumer(consumer); + } + } + + private void suspendChannel() + { + _logger.warn("Suspending channel"); + AMQFrame channelFlowFrame = ChannelFlowBody.createAMQFrame(_channelId, false); + _connection.getProtocolHandler().writeFrame(channelFlowFrame); + } + + private void unsuspendChannel() + { + _logger.warn("Unsuspending channel"); + AMQFrame channelFlowFrame = ChannelFlowBody.createAMQFrame(_channelId, true); + _connection.getProtocolHandler().writeFrame(channelFlowFrame); + } +} diff --git a/java/client/src/org/apache/qpid/client/AMQTemporaryQueue.java b/java/client/src/org/apache/qpid/client/AMQTemporaryQueue.java new file mode 100644 index 0000000000..8c029f4919 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQTemporaryQueue.java @@ -0,0 +1,44 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import javax.jms.JMSException; +import javax.jms.TemporaryQueue; + +/** + * AMQ implementation of a TemporaryQueue. + */ +final class AMQTemporaryQueue extends AMQQueue implements TemporaryQueue { + + /** + * Create a new instance of an AMQTemporaryQueue + */ + public AMQTemporaryQueue() { + super("TempQueue" + Long.toString(System.currentTimeMillis()), + null, true, true); + } + + /** + * @see javax.jms.TemporaryQueue#delete() + */ + public void delete() throws JMSException { + throw new UnsupportedOperationException("Delete not supported, " + + "will auto-delete when connection closed"); + } + +} diff --git a/java/client/src/org/apache/qpid/client/AMQTemporaryTopic.java b/java/client/src/org/apache/qpid/client/AMQTemporaryTopic.java new file mode 100644 index 0000000000..5e96f919e4 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQTemporaryTopic.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import javax.jms.JMSException; +import javax.jms.TemporaryTopic; + +/** + * AMQ implementation of TemporaryTopic. + */ +class AMQTemporaryTopic extends AMQTopic implements TemporaryTopic +{ + + /** + * Create new temporary topic. + */ + public AMQTemporaryTopic() + { + super("TempQueue" + Long.toString(System.currentTimeMillis())); + } + + /** + * @see javax.jms.TemporaryTopic#delete() + */ + public void delete() throws JMSException + { + throw new UnsupportedOperationException("Delete not supported, " + + "will auto-delete when connection closed"); + } + +} diff --git a/java/client/src/org/apache/qpid/client/AMQTopic.java b/java/client/src/org/apache/qpid/client/AMQTopic.java new file mode 100644 index 0000000000..aa6d8f95cf --- /dev/null +++ b/java/client/src/org/apache/qpid/client/AMQTopic.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.url.BindingURL; +import org.apache.qpid.exchange.ExchangeDefaults; + +import javax.jms.JMSException; +import javax.jms.Topic; + +public class AMQTopic extends AMQDestination implements Topic + { + /** + * Constructor for use in creating a topic using a BindingURL. + * + * @param binding The binding url object. + */ + public AMQTopic(BindingURL binding) + { + super(binding); + } + + public AMQTopic(String name) + { + super(ExchangeDefaults.TOPIC_EXCHANGE_NAME, ExchangeDefaults.TOPIC_EXCHANGE_CLASS, name, true, true, null); + _isDurable = false; + } + + /** + * Constructor for use in creating a topic to represent a durable subscription + * @param topic + * @param clientId + * @param subscriptionName + */ + public AMQTopic(AMQTopic topic, String clientId, String subscriptionName) + { + super(ExchangeDefaults.TOPIC_EXCHANGE_NAME, ExchangeDefaults.TOPIC_EXCHANGE_CLASS, topic.getDestinationName(), true, false, clientId + ":" + subscriptionName); + _isDurable = true; + } + + public String getTopicName() throws JMSException + { + return super.getDestinationName(); + } + + public String getEncodedName() + { + return 'T' + getDestinationName(); + } + + public String getRoutingKey() + { + return getDestinationName(); + } + + public boolean isNameRequired() + { + // Topics always rely on a server generated queue name. + return false; + } + + /** + * Override since the queue is always private and we must ensure it remains null. If not, + * reuse of the topic when registering consumers will make all consumers listen on the same (private) queue rather + * than getting their own (private) queue. + * + * This is relatively nasty but it is difficult to come up with a more elegant solution, given + * the requirement in the case on AMQQueue and possibly other AMQDestination subclasses to + * use the underlying queue name even where it is server generated. + */ + public void setQueueName(String queueName) + { + } +} diff --git a/java/client/src/org/apache/qpid/client/BasicMessageConsumer.java b/java/client/src/org/apache/qpid/client/BasicMessageConsumer.java new file mode 100644 index 0000000000..5d13a1cd41 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/BasicMessageConsumer.java @@ -0,0 +1,499 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.message.AbstractJMSMessage; +import org.apache.qpid.client.message.MessageFactoryRegistry; +import org.apache.qpid.client.message.UnprocessedMessage; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.framing.AMQFrame; +import org.apache.qpid.framing.BasicCancelBody; +import org.apache.qpid.framing.BasicCancelOkBody; +import org.apache.qpid.framing.FieldTable; +import org.apache.qpid.jms.MessageConsumer; +import org.apache.qpid.jms.Session; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public class BasicMessageConsumer extends Closeable implements MessageConsumer +{ + private static final Logger _logger = Logger.getLogger(BasicMessageConsumer.class); + + /** + * The connection being used by this consumer + */ + private AMQConnection _connection; + + private String _messageSelector; + + private boolean _noLocal; + + private AMQDestination _destination; + + /** + * When true indicates that a blocking receive call is in progress + */ + private final AtomicBoolean _receiving = new AtomicBoolean(false); + /** + * Holds an atomic reference to the listener installed. + */ + private final AtomicReference _messageListener = new AtomicReference(); + + /** + * The consumer tag allows us to close the consumer by sending a jmsCancel method to the + * broker + */ + private String _consumerTag; + + /** + * We need to know the channel id when constructing frames + */ + private int _channelId; + + /** + * Used in the blocking receive methods to receive a message from + * the Session thread. Argument true indicates we want strict FIFO semantics + */ + private final SynchronousQueue _synchronousQueue = new SynchronousQueue(true); + + private MessageFactoryRegistry _messageFactory; + + private AMQSession _session; + + private AMQProtocolHandler _protocolHandler; + + /** + * We need to store the "raw" field table so that we can resubscribe in the event of failover being required + */ + private FieldTable _rawSelectorFieldTable; + + /** + * We store the prefetch field in order to be able to reuse it when resubscribing in the event of failover + */ + private int _prefetch; + + /** + * We store the exclusive field in order to be able to reuse it when resubscribing in the event of failover + */ + private boolean _exclusive; + + /** + * The acknowledge mode in force for this consumer. Note that the AMQP protocol allows different ack modes + * per consumer whereas JMS defines this at the session level, hence why we associate it with the consumer in our + * implementation. + */ + private int _acknowledgeMode; + + /** + * Number of messages unacknowledged in DUPS_OK_ACKNOWLEDGE mode + */ + private int _outstanding; + + /** + * Tag of last message delievered, whoch should be acknowledged on commit in + * transaction mode. + */ + private long _lastDeliveryTag; + + BasicMessageConsumer(int channelId, AMQConnection connection, AMQDestination destination, String messageSelector, + boolean noLocal, MessageFactoryRegistry messageFactory, AMQSession session, + AMQProtocolHandler protocolHandler, FieldTable rawSelectorFieldTable, int prefetch, + boolean exclusive, int acknowledgeMode) + { + _channelId = channelId; + _connection = connection; + _messageSelector = messageSelector; + _noLocal = noLocal; + _destination = destination; + _messageFactory = messageFactory; + _session = session; + _protocolHandler = protocolHandler; + _rawSelectorFieldTable = rawSelectorFieldTable; + _prefetch = prefetch; + _exclusive = exclusive; + _acknowledgeMode = acknowledgeMode; + } + + public AMQDestination getDestination() + { + return _destination; + } + + public String getMessageSelector() throws JMSException + { + return _messageSelector; + } + + public MessageListener getMessageListener() throws JMSException + { + return (MessageListener) _messageListener.get(); + } + + public int getAcknowledgeMode() + { + return _acknowledgeMode; + } + + private boolean isMessageListenerSet() + { + return _messageListener.get() != null; + } + + public void setMessageListener(MessageListener messageListener) throws JMSException + { + checkNotClosed(); + + //if the current listener is non-null and the session is not stopped, then + //it is an error to call this method. + + //i.e. it is only valid to call this method if + // + // (a) the session is stopped, in which case the dispatcher is not running + // OR + // (b) the listener is null AND we are not receiving synchronously at present + // + + if (_session.isStopped()) + { + _messageListener.set(messageListener); + _logger.debug("Message listener set for destination " + _destination); + } + else + { + if (_receiving.get()) + { + throw new javax.jms.IllegalStateException("Another thread is already receiving synchronously."); + } + if (!_messageListener.compareAndSet(null, messageListener)) + { + throw new javax.jms.IllegalStateException("Attempt to alter listener while session is started."); + } + _logger.debug("Message listener set for destination " + _destination); + + if (messageListener != null) + { + //handle case where connection has already been started, and the dispatcher is blocked + //doing a put on the _synchronousQueue + Object msg = _synchronousQueue.poll(); + if (msg != null) + { + AbstractJMSMessage jmsMsg = (AbstractJMSMessage) msg; + messageListener.onMessage(jmsMsg); + postDeliver(jmsMsg); + } + } + } + } + + private void acquireReceiving() throws JMSException + { + if (!_receiving.compareAndSet(false, true)) + { + throw new javax.jms.IllegalStateException("Another thread is already receiving."); + } + if (isMessageListenerSet()) + { + throw new javax.jms.IllegalStateException("A listener has already been set."); + } + } + + private void releaseReceiving() + { + _receiving.set(false); + } + + public FieldTable getRawSelectorFieldTable() + { + return _rawSelectorFieldTable; + } + + public int getPrefetch() + { + return _prefetch; + } + + public boolean isNoLocal() + { + return _noLocal; + } + + public boolean isExclusive() + { + return _exclusive; + } + + public Message receive() throws JMSException + { + return receive(0); + } + + public Message receive(long l) throws JMSException + { + checkNotClosed(); + + acquireReceiving(); + + try + { + Object o = null; + if (l > 0) + { + o = _synchronousQueue.poll(l, TimeUnit.MILLISECONDS); + } + else + { + o = _synchronousQueue.take(); + } + final AbstractJMSMessage m = returnMessageOrThrow(o); + if (m != null) + { + postDeliver(m); + } + return m; + } + catch (InterruptedException e) + { + return null; + } + finally + { + releaseReceiving(); + } + } + + public Message receiveNoWait() throws JMSException + { + checkNotClosed(); + + acquireReceiving(); + + try + { + Object o = _synchronousQueue.poll(); + final AbstractJMSMessage m = returnMessageOrThrow(o); + if (m != null) + { + postDeliver(m); + } + return m; + } + finally + { + releaseReceiving(); + } + } + + /** + * We can get back either a Message or an exception from the queue. This method examines the argument and deals + * with it by throwing it (if an exception) or returning it (in any other case). + * @param o + * @return a message only if o is a Message + * @throws JMSException if the argument is a throwable. If it is a JMSException it is rethrown as is, but if not + * a JMSException is created with the linked exception set appropriately + */ + private AbstractJMSMessage returnMessageOrThrow(Object o) + throws JMSException + { + // errors are passed via the queue too since there is no way of interrupting the poll() via the API. + if (o instanceof Throwable) + { + JMSException e = new JMSException("Message consumer forcibly closed due to error: " + o); + if (o instanceof Exception) + { + e.setLinkedException((Exception) o); + } + throw e; + } + else + { + return (AbstractJMSMessage) o; + } + } + + public void close() throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + if (!_closed.getAndSet(true)) + { + final AMQFrame cancelFrame = BasicCancelBody.createAMQFrame(_channelId, _consumerTag, false); + + try + { + _protocolHandler.syncWrite(cancelFrame, BasicCancelOkBody.class); + } + catch (AMQException e) + { + _logger.error("Error closing consumer: " + e, e); + throw new JMSException("Error closing consumer: " + e); + } + + deregisterConsumer(); + } + } + } + + /** + * Called when you need to invalidate a consumer. Used for example when failover has occurred and the + * client has vetoed automatic resubscription. + * The caller must hold the failover mutex. + */ + void markClosed() + { + _closed.set(true); + deregisterConsumer(); + } + + /** + * Called from the AMQSession when a message has arrived for this consumer. This methods handles both the case + * of a message listener or a synchronous receive() caller. + * @param messageFrame the raw unprocessed mesage + * @param channelId channel on which this message was sent + */ + void notifyMessage(UnprocessedMessage messageFrame, int channelId) + { + if (_logger.isDebugEnabled()) + { + _logger.debug("notifyMessage called with message number " + messageFrame.deliverBody.deliveryTag); + } + try + { + AbstractJMSMessage jmsMessage = _messageFactory.createMessage(messageFrame.deliverBody.deliveryTag, + messageFrame.deliverBody.redelivered, + messageFrame.contentHeader, + messageFrame.bodies); + + _logger.debug("Message is of type: " + jmsMessage.getClass().getName()); + + preDeliver(jmsMessage); + + if (isMessageListenerSet()) + { + //we do not need a lock around the test above, and the dispatch below as it is invalid + //for an application to alter an installed listener while the session is started + getMessageListener().onMessage(jmsMessage); + postDeliver(jmsMessage); + } + else + { + _synchronousQueue.put(jmsMessage); + } + } + catch (Exception e) + { + if (e instanceof InterruptedException) + { + _logger.info("SynchronousQueue.put interupted. Usually result of connection closing"); + } + else + { + _logger.error("Caught exception (dump follows) - ignoring...", e); + } + } + } + + private void preDeliver(AbstractJMSMessage msg) + { + switch (_acknowledgeMode) + { + case Session.PRE_ACKNOWLEDGE: + _session.acknowledgeMessage(msg.getDeliveryTag(), false); + break; + case Session.CLIENT_ACKNOWLEDGE: + // we set the session so that when the user calls acknowledge() it can call the method on session + // to send out the appropriate frame + msg.setAMQSession(_session); + break; + } + } + + private void postDeliver(AbstractJMSMessage msg) + { + switch (_acknowledgeMode) + { + case Session.DUPS_OK_ACKNOWLEDGE: + if (++_outstanding >= _prefetch) + { + _session.acknowledgeMessage(msg.getDeliveryTag(), true); + } + break; + case Session.AUTO_ACKNOWLEDGE: + _session.acknowledgeMessage(msg.getDeliveryTag(), false); + break; + case Session.SESSION_TRANSACTED: + _lastDeliveryTag = msg.getDeliveryTag(); + break; + } + } + + /** + * Acknowledge up to last message delivered (if any). Used when commiting. + */ + void acknowledgeLastDelivered() + { + if (_lastDeliveryTag > 0) + { + _session.acknowledgeMessage(_lastDeliveryTag, true); + _lastDeliveryTag = -1; + } + } + + void notifyError(Throwable cause) + { + _closed.set(true); + + // we have no way of propagating the exception to a message listener - a JMS limitation - so we + // deal with the case where we have a synchronous receive() waiting for a message to arrive + if (!isMessageListenerSet()) + { + // offer only succeeds if there is a thread waiting for an item from the queue + if (_synchronousQueue.offer(cause)) + { + _logger.debug("Passed exception to synchronous queue for propagation to receive()"); + } + } + deregisterConsumer(); + } + + /** + * Perform cleanup to deregister this consumer. This occurs when closing the consumer in both the clean + * case and in the case of an error occurring. + */ + private void deregisterConsumer() + { + _session.deregisterConsumer(_consumerTag); + } + + public String getConsumerTag() + { + return _consumerTag; + } + + public void setConsumerTag(String consumerTag) + { + _consumerTag = consumerTag; + } +} diff --git a/java/client/src/org/apache/qpid/client/BasicMessageProducer.java b/java/client/src/org/apache/qpid/client/BasicMessageProducer.java new file mode 100644 index 0000000000..9ff6d8564b --- /dev/null +++ b/java/client/src/org/apache/qpid/client/BasicMessageProducer.java @@ -0,0 +1,480 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import org.apache.qpid.AMQException; +import org.apache.qpid.client.message.AbstractJMSMessage; +import org.apache.qpid.client.message.JMSBytesMessage; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.framing.*; +import org.apache.log4j.Logger; +import org.apache.mina.common.ByteBuffer; + +import javax.jms.DeliveryMode; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import java.io.UnsupportedEncodingException; + +public class BasicMessageProducer extends Closeable implements org.apache.qpid.jms.MessageProducer +{ + protected final Logger _logger = Logger.getLogger(getClass()); + + private AMQConnection _connection; + + /** + * If true, messages will not get a timestamp. + */ + private boolean _disableTimestamps; + + /** + * Priority of messages created by this producer. + */ + private int _messagePriority; + + /** + * Time to live of messages. Specified in milliseconds but AMQ has 1 second resolution. + */ + private long _timeToLive; + + /** + * Delivery mode used for this producer. + */ + private int _deliveryMode = DeliveryMode.PERSISTENT; + + /** + * The Destination used for this consumer, if specified upon creation. + */ + protected AMQDestination _destination; + + /** + * Default encoding used for messages produced by this producer. + */ + private String _encoding; + + /** + * Default encoding used for message produced by this producer. + */ + private String _mimeType; + + private AMQProtocolHandler _protocolHandler; + + /** + * True if this producer was created from a transacted session + */ + private boolean _transacted; + + private int _channelId; + + /** + * This is an id generated by the session and is used to tie individual producers to the session. This means we + * can deregister a producer with the session when the producer is clsoed. We need to be able to tie producers + * to the session so that when an error is propagated to the session it can close the producer (meaning that + * a client that happens to hold onto a producer reference will get an error if he tries to use it subsequently). + */ + private long _producerId; + + /** + * The session used to create this producer + */ + private AMQSession _session; + + private final boolean _immediate; + + private final boolean _mandatory; + + private final boolean _waitUntilSent; + + protected BasicMessageProducer(AMQConnection connection, AMQDestination destination, boolean transacted, + int channelId, AMQSession session, AMQProtocolHandler protocolHandler, + long producerId, boolean immediate, boolean mandatory, boolean waitUntilSent) + { + _connection = connection; + _destination = destination; + _transacted = transacted; + _protocolHandler = protocolHandler; + _channelId = channelId; + _session = session; + _producerId = producerId; + if (destination != null) + { + declareDestination(destination); + } + _immediate = immediate; + _mandatory = mandatory; + _waitUntilSent = waitUntilSent; + } + + void resubscribe() throws AMQException + { + if (_destination != null) + { + declareDestination(_destination); + } + } + + private void declareDestination(AMQDestination destination) + { + // Declare the exchange + // Note that the durable and internal arguments are ignored since passive is set to false + AMQFrame declare = ExchangeDeclareBody.createAMQFrame(_channelId, 0, destination.getExchangeName(), + destination.getExchangeClass(), false, + false, false, false, true, null); + _protocolHandler.writeFrame(declare); + } + + public void setDisableMessageID(boolean b) throws JMSException + { + checkNotClosed(); + // IGNORED + } + + public boolean getDisableMessageID() throws JMSException + { + checkNotClosed(); + // Always false for AMQP + return false; + } + + public void setDisableMessageTimestamp(boolean b) throws JMSException + { + checkNotClosed(); + _disableTimestamps = b; + } + + public boolean getDisableMessageTimestamp() throws JMSException + { + checkNotClosed(); + return _disableTimestamps; + } + + public void setDeliveryMode(int i) throws JMSException + { + checkNotClosed(); + if (i != DeliveryMode.NON_PERSISTENT && i != DeliveryMode.PERSISTENT) + { + throw new JMSException("DeliveryMode must be either NON_PERSISTENT or PERSISTENT. Value of " + i + + " is illegal"); + } + _deliveryMode = i; + } + + public int getDeliveryMode() throws JMSException + { + checkNotClosed(); + return _deliveryMode; + } + + public void setPriority(int i) throws JMSException + { + checkNotClosed(); + if (i < 0 || i > 9) + { + throw new IllegalArgumentException("Priority of " + i + " is illegal. Value must be in range 0 to 9"); + } + _messagePriority = i; + } + + public int getPriority() throws JMSException + { + checkNotClosed(); + return _messagePriority; + } + + public void setTimeToLive(long l) throws JMSException + { + checkNotClosed(); + if (l < 0) + { + throw new IllegalArgumentException("Time to live must be non-negative - supplied value was " + l); + } + _timeToLive = l; + } + + public long getTimeToLive() throws JMSException + { + checkNotClosed(); + return _timeToLive; + } + + public Destination getDestination() throws JMSException + { + checkNotClosed(); + return _destination; + } + + public void close() throws JMSException + { + _closed.set(true); + _session.deregisterProducer(_producerId); + } + + public void send(Message message) throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + sendImpl(_destination, (AbstractJMSMessage) message, _deliveryMode, _messagePriority, _timeToLive, + _mandatory, _immediate); + } + } + + public void send(Message message, int deliveryMode) throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + sendImpl(_destination, (AbstractJMSMessage) message, deliveryMode, _messagePriority, _timeToLive, + _mandatory, _immediate); + } + } + + public void send(Message message, int deliveryMode, boolean immediate) throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + sendImpl(_destination, (AbstractJMSMessage) message, deliveryMode, _messagePriority, _timeToLive, + _mandatory, immediate); + } + } + + public void send(Message message, int deliveryMode, int priority, + long timeToLive) throws JMSException + { + synchronized (_connection.getFailoverMutex()) + { + sendImpl(_destination, (AbstractJMSMessage)message, deliveryMode, priority, timeToLive, _mandatory, + _immediate); + } + } + + public void send(Destination destination, Message message) throws JMSException + { + checkNotClosed(); + synchronized (_connection.getFailoverMutex()) + { + validateDestination(destination); + sendImpl((AMQDestination) destination, (AbstractJMSMessage) message, _deliveryMode, _messagePriority, _timeToLive, + _mandatory, _immediate); + } + } + + public void send(Destination destination, Message message, int deliveryMode, + int priority, long timeToLive) + throws JMSException + { + checkNotClosed(); + synchronized (_connection.getFailoverMutex()) + { + validateDestination(destination); + sendImpl((AMQDestination) destination, (AbstractJMSMessage) message, deliveryMode, priority, timeToLive, + _mandatory, _immediate); + } + } + + public void send(Destination destination, Message message, int deliveryMode, + int priority, long timeToLive, boolean mandatory) + throws JMSException + { + checkNotClosed(); + synchronized (_connection.getFailoverMutex()) + { + validateDestination(destination); + sendImpl((AMQDestination) destination, (AbstractJMSMessage) message, deliveryMode, priority, timeToLive, + mandatory, _immediate); + } + } + + public void send(Destination destination, Message message, int deliveryMode, + int priority, long timeToLive, boolean mandatory, boolean immediate) + throws JMSException + { + checkNotClosed(); + synchronized (_connection.getFailoverMutex()) + { + validateDestination(destination); + sendImpl((AMQDestination) destination, (AbstractJMSMessage) message, deliveryMode, priority, timeToLive, + mandatory, immediate); + } + } + + public void send(Destination destination, Message message, int deliveryMode, + int priority, long timeToLive, boolean mandatory, + boolean immediate, boolean waitUntilSent) + throws JMSException + { + checkNotClosed(); + synchronized (_connection.getFailoverMutex()) + { + validateDestination(destination); + sendImpl((AMQDestination) destination, (AbstractJMSMessage) message, deliveryMode, priority, timeToLive, + mandatory, immediate, waitUntilSent); + } + } + + private void validateDestination(Destination destination) throws JMSException + { + if (!(destination instanceof AMQDestination)) + { + throw new JMSException("Unsupported destination class: " + + (destination != null?destination.getClass():null)); + } + declareDestination((AMQDestination)destination); + } + + protected void sendImpl(AMQDestination destination, AbstractJMSMessage message, int deliveryMode, int priority, + long timeToLive, boolean mandatory, boolean immediate) throws JMSException + { + sendImpl(destination, message, deliveryMode, priority, timeToLive, mandatory, immediate, _waitUntilSent); + } + /** + * The caller of this method must hold the failover mutex. + * @param destination + * @param message + * @param deliveryMode + * @param priority + * @param timeToLive + * @param mandatory + * @param immediate + * @throws JMSException + */ + protected void sendImpl(AMQDestination destination, AbstractJMSMessage message, int deliveryMode, int priority, + long timeToLive, boolean mandatory, boolean immediate, boolean wait) throws JMSException + { + AMQFrame publishFrame = BasicPublishBody.createAMQFrame(_channelId, 0, destination.getExchangeName(), + destination.getRoutingKey(), mandatory, immediate); + + long currentTime = 0; + if (!_disableTimestamps) + { + currentTime = System.currentTimeMillis(); + message.setJMSTimestamp(currentTime); + } + // + // Very nasty temporary hack for GRM-206. Will be altered ASAP. + // + if(message instanceof JMSBytesMessage) + { + JMSBytesMessage msg = (JMSBytesMessage) message; + if(!msg.isReadable()) + { + msg.reset(); + } + } + ByteBuffer payload = message.getData(); + BasicContentHeaderProperties contentHeaderProperties = message.getJmsContentHeaderProperties(); + + if (timeToLive > 0) + { + if (!_disableTimestamps) + { + contentHeaderProperties.setExpiration(currentTime + timeToLive); + } + } + else + { + if (!_disableTimestamps) + { + contentHeaderProperties.setExpiration(0); + } + } + contentHeaderProperties.setDeliveryMode((byte) deliveryMode); + contentHeaderProperties.setPriority((byte) priority); + + int size = payload.limit(); + ContentBody[] contentBodies = createContentBodies(payload); + AMQFrame[] frames = new AMQFrame[2 + contentBodies.length]; + for (int i = 0; i < contentBodies.length; i++) + { + frames[2 + i] = ContentBody.createAMQFrame(_channelId, contentBodies[i]); + } + if (contentBodies.length > 0 && _logger.isDebugEnabled()) + { + _logger.debug("Sending content body frames to " + destination); + } + + // weight argument of zero indicates no child content headers, just bodies + AMQFrame contentHeaderFrame = ContentHeaderBody.createAMQFrame(_channelId, BasicConsumeBody.CLASS_ID, 0, + contentHeaderProperties, + size); + if (_logger.isDebugEnabled()) + { + _logger.debug("Sending content header frame to " + destination); + } + + frames[0] = publishFrame; + frames[1] = contentHeaderFrame; + CompositeAMQDataBlock compositeFrame = new CompositeAMQDataBlock(frames); + _protocolHandler.writeFrame(compositeFrame, wait); + } + + /** + * Create content bodies. This will split a large message into numerous bodies depending on the negotiated + * maximum frame size. + * @param payload + * @return the array of content bodies + */ + private ContentBody[] createContentBodies(ByteBuffer payload) + { + if (payload == null) + { + return null; + } + else if (payload.remaining() == 0) + { + return new ContentBody[0]; + } + // we substract one from the total frame maximum size to account for the end of frame marker in a body frame + // (0xCE byte). + int dataLength = payload.remaining(); + final long framePayloadMax = _session.getAMQConnection().getMaximumFrameSize() - 1; + int lastFrame = (dataLength % framePayloadMax) > 0 ? 1 : 0; + int frameCount = (int) (dataLength/framePayloadMax) + lastFrame; + final ContentBody[] bodies = new ContentBody[frameCount]; + + if (frameCount == 1) + { + bodies[0] = new ContentBody(); + bodies[0].payload = payload; + } + else + { + long remaining = dataLength; + for (int i = 0; i < bodies.length; i++) + { + bodies[i] = new ContentBody(); + payload.position((int)framePayloadMax * i); + int length = (remaining >= framePayloadMax) ? (int)framePayloadMax : (int)remaining; + payload.limit(payload.position() + length); + bodies[i].payload = payload.slice(); + remaining -= length; + } + } + return bodies; + } + + public void setMimeType(String mimeType) + { + checkNotClosed(); + _mimeType = mimeType; + } + + public void setEncoding(String encoding) throws UnsupportedEncodingException + { + checkNotClosed(); + _encoding = encoding; + } +} diff --git a/java/client/src/org/apache/qpid/client/Closeable.java b/java/client/src/org/apache/qpid/client/Closeable.java new file mode 100644 index 0000000000..0381823b69 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/Closeable.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import javax.jms.JMSException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provides support for orderly shutdown of an object. + */ +public abstract class Closeable +{ + /** + * We use an atomic boolean so that we do not have to synchronized access to this flag. Synchronizing + * access to this flag would mean have a synchronized block in every method. + */ + protected final AtomicBoolean _closed = new AtomicBoolean(false); + + protected void checkNotClosed() + { + if (_closed.get()) + { + throw new IllegalStateException("Object " + toString() + " has been closed"); + } + } + + public boolean isClosed() + { + return _closed.get(); + } + + public abstract void close() throws JMSException; +} diff --git a/java/client/src/org/apache/qpid/client/ConnectionTuneParameters.java b/java/client/src/org/apache/qpid/client/ConnectionTuneParameters.java new file mode 100644 index 0000000000..a3471e9140 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/ConnectionTuneParameters.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +public class ConnectionTuneParameters +{ + private long _frameMax; + + private int _channelMax; + + private int _heartbeat; + + private long _txnLimit; + + public long getFrameMax() + { + return _frameMax; + } + + public void setFrameMax(long frameMax) + { + _frameMax = frameMax; + } + + public int getChannelMax() + { + return _channelMax; + } + + public void setChannelMax(int channelMax) + { + _channelMax = channelMax; + } + + public int getHeartbeat() + { + return _heartbeat; + } + + public void setHeartbeat(int hearbeat) + { + _heartbeat = hearbeat; + } + + public long getTxnLimit() + { + return _txnLimit; + } + + public void setTxnLimit(long txnLimit) + { + _txnLimit = txnLimit; + } +} diff --git a/java/client/src/org/apache/qpid/client/TopicSubscriberAdaptor.java b/java/client/src/org/apache/qpid/client/TopicSubscriberAdaptor.java new file mode 100644 index 0000000000..caa7e5139b --- /dev/null +++ b/java/client/src/org/apache/qpid/client/TopicSubscriberAdaptor.java @@ -0,0 +1,91 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Topic; +import javax.jms.TopicSubscriber; + +/** + * Wraps a MessageConsumer to fulfill the extended TopicSubscriber contract + * + */ +class TopicSubscriberAdaptor implements TopicSubscriber +{ + private final Topic _topic; + private final MessageConsumer _consumer; + private final boolean _noLocal; + + TopicSubscriberAdaptor(Topic topic, MessageConsumer consumer, boolean noLocal) + { + _topic = topic; + _consumer = consumer; + _noLocal = noLocal; + } + TopicSubscriberAdaptor(Topic topic, BasicMessageConsumer consumer) + { + this(topic, consumer, consumer.isNoLocal()); + } + public Topic getTopic() throws JMSException + { + return _topic; + } + + public boolean getNoLocal() throws JMSException + { + return _noLocal; + } + + public String getMessageSelector() throws JMSException + { + return _consumer.getMessageSelector(); + } + + public MessageListener getMessageListener() throws JMSException + { + return _consumer.getMessageListener(); + } + + public void setMessageListener(MessageListener messageListener) throws JMSException + { + _consumer.setMessageListener(messageListener); + } + + public Message receive() throws JMSException + { + return _consumer.receive(); + } + + public Message receive(long l) throws JMSException + { + return _consumer.receive(l); + } + + public Message receiveNoWait() throws JMSException + { + return _consumer.receiveNoWait(); + } + + public void close() throws JMSException + { + _consumer.close(); + } +} diff --git a/java/client/src/org/apache/qpid/client/failover/FailoverException.java b/java/client/src/org/apache/qpid/client/failover/FailoverException.java new file mode 100644 index 0000000000..c17c56bdf7 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/failover/FailoverException.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.failover; + +/** + * This exception is thrown when failover is taking place and we need to let other + * parts of the client know about this. + */ +public class FailoverException extends RuntimeException +{ + public FailoverException(String message) + { + super(message); + } +} diff --git a/java/client/src/org/apache/qpid/client/failover/FailoverHandler.java b/java/client/src/org/apache/qpid/client/failover/FailoverHandler.java new file mode 100644 index 0000000000..0c68c75122 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/failover/FailoverHandler.java @@ -0,0 +1,180 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.failover; + +import org.apache.mina.common.IoSession; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.failover.FailoverException; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.client.failover.FailoverState; +import org.apache.qpid.AMQDisconnectedException; +import org.apache.log4j.Logger; + +import java.util.concurrent.CountDownLatch; + +/** + * When failover is required, we need a separate thread to handle the establishment of the new connection and + * the transfer of subscriptions. + * </p> + * The reason this needs to be a separate thread is because you cannot do this work inside the MINA IO processor + * thread. One significant task is the connection setup which involves a protocol exchange until a particular state + * is achieved. However if you do this in the MINA thread, you have to block until the state is achieved which means + * the IO processor is not able to do anything at all. + */ +public class FailoverHandler implements Runnable +{ + private static final Logger _logger = Logger.getLogger(FailoverHandler.class); + + private final IoSession _session; + private AMQProtocolHandler _amqProtocolHandler; + + /** + * Used where forcing the failover host + */ + private String _host; + + /** + * Used where forcing the failover port + */ + private int _port; + + public FailoverHandler(AMQProtocolHandler amqProtocolHandler, IoSession session) + { + _amqProtocolHandler = amqProtocolHandler; + _session = session; + } + + public void run() + { + if (Thread.currentThread().isDaemon()) + { + throw new IllegalStateException("FailoverHandler must run on a non-daemon thread."); + } + //Thread.currentThread().setName("Failover Thread"); + + _amqProtocolHandler.setFailoverLatch(new CountDownLatch(1)); + + // We wake up listeners. If they can handle failover, they will extend the + // FailoverSupport class and will in turn block on the latch until failover + // has completed before retrying the operation + _amqProtocolHandler.propagateExceptionToWaiters(new FailoverException("Failing over about to start")); + + // Since failover impacts several structures we protect them all with a single mutex. These structures + // are also in child objects of the connection. This allows us to manipulate them without affecting + // client code which runs in a separate thread. + synchronized (_amqProtocolHandler.getConnection().getFailoverMutex()) + { + _logger.info("Starting failover process"); + + // We switch in a new state manager temporarily so that the interaction to get to the "connection open" + // state works, without us having to terminate any existing "state waiters". We could theoretically + // have a state waiter waiting until the connection is closed for some reason. Or in future we may have + // a slightly more complex state model therefore I felt it was worthwhile doing this. + AMQStateManager existingStateManager = _amqProtocolHandler.getStateManager(); + _amqProtocolHandler.setStateManager(new AMQStateManager()); + if (!_amqProtocolHandler.getConnection().firePreFailover(_host != null)) + { + _amqProtocolHandler.setStateManager(existingStateManager); + if (_host != null) + { + _amqProtocolHandler.getConnection().exceptionReceived(new AMQDisconnectedException("Redirect was vetoed by client")); + } + else + { + _amqProtocolHandler.getConnection().exceptionReceived(new AMQDisconnectedException("Failover was vetoed by client")); + } + _amqProtocolHandler.getFailoverLatch().countDown(); + _amqProtocolHandler.setFailoverLatch(null); + return; + } + boolean failoverSucceeded; + // when host is non null we have a specified failover host otherwise we all the client to cycle through + // all specified hosts + + // if _host has value then we are performing a redirect. + if (_host != null) + { + failoverSucceeded = _amqProtocolHandler.getConnection().attemptReconnection(_host, _port, _amqProtocolHandler.isUseSSL()); + } + else + { + failoverSucceeded = _amqProtocolHandler.getConnection().attemptReconnection(); + } + if (!failoverSucceeded) + { + _amqProtocolHandler.setStateManager(existingStateManager); + _amqProtocolHandler.getConnection().exceptionReceived( + new AMQDisconnectedException("Server closed connection and no failover " + + "was successful")); + } + else + { + _amqProtocolHandler.setStateManager(existingStateManager); + try + { + if (_amqProtocolHandler.getConnection().firePreResubscribe()) + { + _logger.info("Resubscribing on new connection"); + _amqProtocolHandler.getConnection().resubscribeSessions(); + } + else + { + _logger.info("Client vetoed automatic resubscription"); + } + _amqProtocolHandler.getConnection().fireFailoverComplete(); + _amqProtocolHandler.setFailoverState(FailoverState.NOT_STARTED); + _logger.info("Connection failover completed successfully"); + } + catch (Exception e) + { + _logger.info("Failover process failed - exception being propagated by protocol handler"); + _amqProtocolHandler.setFailoverState(FailoverState.FAILED); + try + { + _amqProtocolHandler.exceptionCaught(_session, e); + } + catch (Exception ex) + { + _logger.error("Error notifying protocol session of error: " + ex, ex); + } + } + } + } + _amqProtocolHandler.getFailoverLatch().countDown(); + } + + public String getHost() + { + return _host; + } + + public void setHost(String host) + { + _host = host; + } + + public int getPort() + { + return _port; + } + + public void setPort(int port) + { + _port = port; + } +} diff --git a/java/client/src/org/apache/qpid/client/failover/FailoverState.java b/java/client/src/org/apache/qpid/client/failover/FailoverState.java new file mode 100644 index 0000000000..20886f391c --- /dev/null +++ b/java/client/src/org/apache/qpid/client/failover/FailoverState.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.failover; + +/** + * Enumeration of failover states. Used to handle failover from within AMQProtocolHandler where MINA events need to be + * dealt with and can happen during failover. + */ +public final class FailoverState +{ + private final String _state; + + /** Failover has not yet been attempted on this connection */ + public static final FailoverState NOT_STARTED = new FailoverState("NOT STARTED"); + + /** Failover has been requested on this connection but has not completed */ + public static final FailoverState IN_PROGRESS = new FailoverState("IN PROGRESS"); + + /** Failover has been attempted and failed */ + public static final FailoverState FAILED = new FailoverState("FAILED"); + + private FailoverState(String state) + { + _state = state; + } + + public String toString() + { + return "FailoverState: " + _state; + } +} diff --git a/java/client/src/org/apache/qpid/client/failover/FailoverSupport.java b/java/client/src/org/apache/qpid/client/failover/FailoverSupport.java new file mode 100644 index 0000000000..e2d797dfac --- /dev/null +++ b/java/client/src/org/apache/qpid/client/failover/FailoverSupport.java @@ -0,0 +1,63 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.failover; + +import org.apache.log4j.Logger; +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.client.failover.FailoverException; + +import javax.jms.JMSException; + +public abstract class FailoverSupport +{ + private static final Logger _log = Logger.getLogger(FailoverSupport.class); + + public Object execute(AMQConnection con) throws JMSException + { + // We wait until we are not in the middle of failover before acquiring the mutex and then proceeding. + // Any method that can potentially block for any reason should use this class so that deadlock will not + // occur. The FailoverException is propagated by the AMQProtocolHandler to any listeners (e.g. frame listeners) + // that might be causing a block. When that happens, the exception is caught here and the mutex is released + // before waiting for the failover to complete (either successfully or unsuccessfully). + while (true) + { + try + { + con.blockUntilNotFailingOver(); + } + catch (InterruptedException e) + { + _log.info("Interrupted: " + e, e); + return null; + } + synchronized (con.getFailoverMutex()) + { + try + { + return operation(); + } + catch (FailoverException e) + { + _log.info("Failover exception caught during operation"); + } + } + } + } + + protected abstract Object operation() throws JMSException; +} diff --git a/java/client/src/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java new file mode 100644 index 0000000000..ab1e84ee17 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.BasicDeliverBody; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.message.UnprocessedMessage; + +public class BasicDeliverMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(BasicDeliverMethodHandler.class); + + private static final BasicDeliverMethodHandler _instance = new BasicDeliverMethodHandler(); + + public static BasicDeliverMethodHandler getInstance() + { + return _instance; + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + final UnprocessedMessage msg = new UnprocessedMessage(); + msg.deliverBody = (BasicDeliverBody) evt.getMethod(); + msg.channelId = evt.getChannelId(); + _logger.debug("New JmsDeliver method received"); + evt.getProtocolSession().unprocessedMessageReceived(msg); + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/BasicReturnMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/BasicReturnMethodHandler.java new file mode 100644 index 0000000000..ae9d7bcb4a --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/BasicReturnMethodHandler.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.message.UnprocessedMessage; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.BasicReturnBody; + +public class BasicReturnMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(BasicReturnMethodHandler.class); + + private static final BasicReturnMethodHandler _instance = new BasicReturnMethodHandler(); + + public static BasicReturnMethodHandler getInstance() + { + return _instance; + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + _logger.debug("New JmsBounce method received"); + final UnprocessedMessage msg = new UnprocessedMessage(); + msg.deliverBody = null; + msg.bounceBody = (BasicReturnBody) evt.getMethod(); + msg.channelId = evt.getChannelId(); + + evt.getProtocolSession().unprocessedMessageReceived(msg); + } +}
\ No newline at end of file diff --git a/java/client/src/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java new file mode 100644 index 0000000000..959f1eb4df --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQChannelClosedException; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.AMQNoConsumersException; +import org.apache.qpid.client.AMQNoRouteException; +import org.apache.qpid.protocol.AMQConstant; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.AMQFrame; +import org.apache.qpid.framing.ChannelCloseBody; +import org.apache.qpid.framing.ChannelCloseOkBody; + +public class ChannelCloseMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(ChannelCloseMethodHandler.class); + + private static ChannelCloseMethodHandler _handler = new ChannelCloseMethodHandler(); + + public static ChannelCloseMethodHandler getInstance() + { + return _handler; + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + _logger.debug("ChannelClose method received"); + ChannelCloseBody method = (ChannelCloseBody) evt.getMethod(); + + int errorCode = method.replyCode; + String reason = method.replyText; + if (_logger.isDebugEnabled()) + { + _logger.debug("Channel close reply code: " + errorCode + ", reason: " + reason); + } + + AMQFrame frame = ChannelCloseOkBody.createAMQFrame(evt.getChannelId()); + evt.getProtocolSession().writeFrame(frame); + if (errorCode != AMQConstant.REPLY_SUCCESS.getCode()) + { + _logger.error("Channel close received with errorCode " + errorCode + ", and reason " + reason); + if (errorCode == AMQConstant.NO_CONSUMERS.getCode()) + { + throw new AMQNoConsumersException("Error: " + reason, null); + } + else + { + if (errorCode == AMQConstant.NO_ROUTE.getCode()) + { + throw new AMQNoRouteException("Error: " + reason, null); + } + else + { + throw new AMQChannelClosedException(errorCode, "Error: " + reason); + } + } + } + evt.getProtocolSession().channelClosed(evt.getChannelId(), errorCode, reason); + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java new file mode 100644 index 0000000000..b857b5e91b --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.qpid.AMQException; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.log4j.Logger; + +public class ChannelCloseOkMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(ChannelCloseOkMethodHandler.class); + + private static final ChannelCloseOkMethodHandler _instance = new ChannelCloseOkMethodHandler(); + + public static ChannelCloseOkMethodHandler getInstance() + { + return _instance; + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + _logger.info("Received channel-close-ok for channel-id " + evt.getChannelId()); + + //todo this should do the closure + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java new file mode 100644 index 0000000000..f26917b2e3 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.ChannelFlowOkBody; + +public class ChannelFlowOkMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(ChannelFlowOkMethodHandler.class); + private static final ChannelFlowOkMethodHandler _instance = new ChannelFlowOkMethodHandler(); + + public static ChannelFlowOkMethodHandler getInstance() + { + return _instance; + } + + private ChannelFlowOkMethodHandler() + { + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + ChannelFlowOkBody method = (ChannelFlowOkBody) evt.getMethod(); + _logger.debug("Received Channel.Flow-Ok message, active = " + method.active); + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java new file mode 100644 index 0000000000..6ec5b1cb49 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.AMQConnectionClosedException; +import org.apache.qpid.protocol.AMQConstant; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.client.AMQAuthenticationException; +import org.apache.qpid.framing.ConnectionCloseBody; +import org.apache.qpid.framing.ConnectionCloseOkBody; + +public class ConnectionCloseMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(ConnectionCloseMethodHandler.class); + + private static ConnectionCloseMethodHandler _handler = new ConnectionCloseMethodHandler(); + + public static ConnectionCloseMethodHandler getInstance() + { + return _handler; + } + + private ConnectionCloseMethodHandler() + { + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + _logger.info("ConnectionClose frame received"); + ConnectionCloseBody method = (ConnectionCloseBody) evt.getMethod(); + + // does it matter + //stateManager.changeState(AMQState.CONNECTION_CLOSING); + + int errorCode = method.replyCode; + String reason = method.replyText; + + // TODO: check whether channel id of zero is appropriate + evt.getProtocolSession().writeFrame(ConnectionCloseOkBody.createAMQFrame((short)0)); + + if (errorCode != 200) + { + if(errorCode == AMQConstant.NOT_ALLOWED.getCode()) + { + _logger.info("Authentication Error:"+Thread.currentThread().getName()); + + evt.getProtocolSession().closeProtocolSession(); + + //todo this is a bit of a fudge (could be conssidered such as each new connection needs a new state manager or at least a fresh state. + stateManager.changeState(AMQState.CONNECTION_NOT_STARTED); + + throw new AMQAuthenticationException(errorCode, reason); + } + else + { + _logger.info("Connection close received with error code " + errorCode); + + + throw new AMQConnectionClosedException(errorCode, "Error: " + reason); + } + } + + // this actually closes the connection in the case where it is not an error. + + evt.getProtocolSession().closeProtocolSession(); + + stateManager.changeState(AMQState.CONNECTION_CLOSED); + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java new file mode 100644 index 0000000000..73986ed81a --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.protocol.AMQProtocolSession; +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.ConnectionOpenOkBody; + +public class ConnectionOpenOkMethodHandler implements StateAwareMethodListener +{ + + private static final Logger _logger = Logger.getLogger(ConnectionOpenOkMethodHandler.class); + + private static final ConnectionOpenOkMethodHandler _instance = new ConnectionOpenOkMethodHandler(); + + public static ConnectionOpenOkMethodHandler getInstance() + { + return _instance; + } + + private ConnectionOpenOkMethodHandler() + { + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + AMQProtocolSession session = evt.getProtocolSession(); + ConnectionOpenOkBody method = (ConnectionOpenOkBody) evt.getMethod(); + stateManager.changeState(AMQState.CONNECTION_OPEN); + } + +} diff --git a/java/client/src/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java new file mode 100644 index 0000000000..d326191d84 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.ConnectionRedirectBody; + +public class ConnectionRedirectMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(ConnectionRedirectMethodHandler.class); + + private static final int DEFAULT_REDIRECT_PORT = 5672; + + private static ConnectionRedirectMethodHandler _handler = new ConnectionRedirectMethodHandler(); + + public static ConnectionRedirectMethodHandler getInstance() + { + return _handler; + } + + private ConnectionRedirectMethodHandler() + { + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + _logger.info("ConnectionRedirect frame received"); + ConnectionRedirectBody method = (ConnectionRedirectBody) evt.getMethod(); + + // the host is in the form hostname:port with the port being optional + int portIndex = method.host.indexOf(':'); + String host; + int port; + if (portIndex == -1) + { + host = method.host; + port = DEFAULT_REDIRECT_PORT; + } + else + { + host = method.host.substring(0, portIndex); + port = Integer.parseInt(method.host.substring(portIndex + 1)); + } + evt.getProtocolSession().failover(host, port); + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java new file mode 100644 index 0000000000..106ffc908e --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java @@ -0,0 +1,64 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.AMQFrame; +import org.apache.qpid.framing.ConnectionSecureOkBody; +import org.apache.qpid.framing.ConnectionSecureBody; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.client.protocol.AMQMethodEvent; + +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; + +public class ConnectionSecureMethodHandler implements StateAwareMethodListener +{ + private static final ConnectionSecureMethodHandler _instance = new ConnectionSecureMethodHandler(); + + public static ConnectionSecureMethodHandler getInstance() + { + return _instance; + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + SaslClient client = evt.getProtocolSession().getSaslClient(); + if (client == null) + { + throw new AMQException("No SASL client set up - cannot proceed with authentication"); + } + + ConnectionSecureBody body = (ConnectionSecureBody) evt.getMethod(); + + try + { + // Evaluate server challenge + byte[] response = client.evaluateChallenge(body.challenge); + AMQFrame responseFrame = ConnectionSecureOkBody.createAMQFrame(evt.getChannelId(), response); + evt.getProtocolSession().writeFrame(responseFrame); + } + catch (SaslException e) + { + throw new AMQException("Error processing SASL challenge: " + e, e); + } + + + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java new file mode 100644 index 0000000000..f0d17e9b55 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java @@ -0,0 +1,184 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.protocol.AMQProtocolSession; +import org.apache.qpid.client.security.AMQCallbackHandler; +import org.apache.qpid.client.security.CallbackHandlerRegistry; +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.ConnectionStartBody; +import org.apache.qpid.framing.ConnectionStartOkBody; +import org.apache.qpid.framing.FieldTable; + +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import java.io.UnsupportedEncodingException; +import java.util.HashSet; +import java.util.StringTokenizer; + +public class ConnectionStartMethodHandler implements StateAwareMethodListener +{ + + private static final Logger _log = Logger.getLogger(ConnectionStartMethodHandler.class); + + private static final ConnectionStartMethodHandler _instance = new ConnectionStartMethodHandler(); + + public static ConnectionStartMethodHandler getInstance() + { + return _instance; + } + + private ConnectionStartMethodHandler() + { + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + ConnectionStartBody body = (ConnectionStartBody) evt.getMethod(); + + try + { + // the mechanism we are going to use + String mechanism; + if (body.mechanisms == null) + { + throw new AMQException("mechanism not specified in ConnectionStart method frame"); + } + else + { + mechanism = chooseMechanism(body.mechanisms); + } + + if (mechanism == null) + { + throw new AMQException("No supported security mechanism found, passed: " + new String(body.mechanisms)); + } + + final AMQProtocolSession ps = evt.getProtocolSession(); + byte[] saslResponse; + try + { + SaslClient sc = Sasl.createSaslClient(new String[]{mechanism}, + null, "AMQP", "localhost", + null, createCallbackHandler(mechanism, ps)); + if (sc == null) + { + throw new AMQException("Client SASL configuration error: no SaslClient could be created for mechanism " + + mechanism + ". Please ensure all factories are registered. See DynamicSaslRegistrar for " + + " details of how to register non-standard SASL client providers."); + } + ps.setSaslClient(sc); + saslResponse = (sc.hasInitialResponse() ? sc.evaluateChallenge(new byte[0]) : null); + } + catch (SaslException e) + { + ps.setSaslClient(null); + throw new AMQException("Unable to create SASL client: " + e, e); + } + + if (body.locales == null) + { + throw new AMQException("Locales is not defined in Connection Start method"); + } + final String locales = new String(body.locales, "utf8"); + final StringTokenizer tokenizer = new StringTokenizer(locales, " "); + String selectedLocale = null; + if (tokenizer.hasMoreTokens()) + { + selectedLocale = tokenizer.nextToken(); + } + else + { + throw new AMQException("No locales sent from server, passed: " + locales); + } + + stateManager.changeState(AMQState.CONNECTION_NOT_TUNED); + FieldTable clientProperties = new FieldTable(); + clientProperties.put("instance", ps.getClientID()); + clientProperties.put("product", "Qpid"); + clientProperties.put("version", "1.0"); + clientProperties.put("platform", getFullSystemInfo()); + ps.writeFrame(ConnectionStartOkBody.createAMQFrame(evt.getChannelId(), clientProperties, mechanism, + saslResponse, selectedLocale)); + } + catch (UnsupportedEncodingException e) + { + throw new AMQException(_log, "Unable to decode data: " + e, e); + } + } + + private String getFullSystemInfo() + { + StringBuffer fullSystemInfo = new StringBuffer(); + fullSystemInfo.append(System.getProperty("java.runtime.name")); + fullSystemInfo.append(", " + System.getProperty("java.runtime.version")); + fullSystemInfo.append(", " + System.getProperty("java.vendor")); + fullSystemInfo.append(", " + System.getProperty("os.arch")); + fullSystemInfo.append(", " + System.getProperty("os.name")); + fullSystemInfo.append(", " + System.getProperty("os.version")); + fullSystemInfo.append(", " + System.getProperty("sun.os.patch.level")); + + return fullSystemInfo.toString(); + } + + private String chooseMechanism(byte[] availableMechanisms) throws UnsupportedEncodingException + { + final String mechanisms = new String(availableMechanisms, "utf8"); + StringTokenizer tokenizer = new StringTokenizer(mechanisms, " "); + HashSet mechanismSet = new HashSet(); + while (tokenizer.hasMoreTokens()) + { + mechanismSet.add(tokenizer.nextToken()); + } + + String preferredMechanisms = CallbackHandlerRegistry.getInstance().getMechanisms(); + StringTokenizer prefTokenizer = new StringTokenizer(preferredMechanisms, " "); + while (prefTokenizer.hasMoreTokens()) + { + String mech = prefTokenizer.nextToken(); + if (mechanismSet.contains(mech)) + { + return mech; + } + } + return null; + } + + private AMQCallbackHandler createCallbackHandler(String mechanism, AMQProtocolSession protocolSession) + throws AMQException + { + Class mechanismClass = CallbackHandlerRegistry.getInstance().getCallbackHandlerClass(mechanism); + try + { + Object instance = mechanismClass.newInstance(); + AMQCallbackHandler cbh = (AMQCallbackHandler) instance; + cbh.initialise(protocolSession); + return cbh; + } + catch (Exception e) + { + throw new AMQException("Unable to create callback handler: " + e, e); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java b/java/client/src/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java new file mode 100644 index 0000000000..6e32bb692e --- /dev/null +++ b/java/client/src/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.handler; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.ConnectionTuneParameters; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.protocol.AMQProtocolSession; +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.StateAwareMethodListener; +import org.apache.qpid.framing.ConnectionOpenBody; +import org.apache.qpid.framing.ConnectionTuneBody; +import org.apache.qpid.framing.ConnectionTuneOkBody; +import org.apache.qpid.framing.AMQFrame; + +public class ConnectionTuneMethodHandler implements StateAwareMethodListener +{ + private static final Logger _logger = Logger.getLogger(ConnectionTuneMethodHandler.class); + + private static final ConnectionTuneMethodHandler _instance = new ConnectionTuneMethodHandler(); + + public static ConnectionTuneMethodHandler getInstance() + { + return _instance; + } + + protected ConnectionTuneMethodHandler() + { + } + + public void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException + { + _logger.debug("ConnectionTune frame received"); + ConnectionTuneBody frame = (ConnectionTuneBody) evt.getMethod(); + AMQProtocolSession session = evt.getProtocolSession(); + + ConnectionTuneParameters params = session.getConnectionTuneParameters(); + if (params == null) + { + params = new ConnectionTuneParameters(); + } + + params.setFrameMax(frame.frameMax); + params.setChannelMax(frame.channelMax); + params.setHeartbeat(Integer.getInteger("amqj.heartbeat.delay", frame.heartbeat)); + session.setConnectionTuneParameters(params); + + stateManager.changeState(AMQState.CONNECTION_NOT_OPENED); + session.writeFrame(createTuneOkFrame(evt.getChannelId(), params)); + session.writeFrame(createConnectionOpenFrame(evt.getChannelId(), session.getAMQConnection().getVirtualHost(), null, true)); + } + + protected AMQFrame createConnectionOpenFrame(int channel, String path, String capabilities, boolean insist) + { + return ConnectionOpenBody.createAMQFrame(channel, path, capabilities, insist); + } + + protected AMQFrame createTuneOkFrame(int channel, ConnectionTuneParameters params) + { + return ConnectionTuneOkBody.createAMQFrame(channel, params.getChannelMax(), params.getFrameMax(), params.getHeartbeat()); + } +} diff --git a/java/client/src/org/apache/qpid/client/message/AMQMessage.java b/java/client/src/org/apache/qpid/client/message/AMQMessage.java new file mode 100644 index 0000000000..d281a15607 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/AMQMessage.java @@ -0,0 +1,68 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.framing.ContentHeaderProperties; +import org.apache.qpid.client.AMQSession; + +public class AMQMessage +{ + protected ContentHeaderProperties _contentHeaderProperties; + + /** + * If the acknowledge mode is CLIENT_ACKNOWLEDGE the session is required + */ + protected AMQSession _session; + + protected final long _deliveryTag; + + public AMQMessage(ContentHeaderProperties properties, long deliveryTag) + { + _contentHeaderProperties = properties; + _deliveryTag = deliveryTag; + } + + public AMQMessage(ContentHeaderProperties properties) + { + this(properties, -1); + } + + /** + * The session is set when CLIENT_ACKNOWLEDGE mode is used so that the CHANNEL ACK can be sent when the user + * calls acknowledge() + * @param s the AMQ session that delivered this message + */ + public void setAMQSession(AMQSession s) + { + _session = s; + } + + public AMQSession getAMQSession() + { + return _session; + } + + /** + * Get the AMQ message number assigned to this message + * @return the message number + */ + public long getDeliveryTag() + { + return _deliveryTag; + } +} diff --git a/java/client/src/org/apache/qpid/client/message/AbstractJMSMessage.java b/java/client/src/org/apache/qpid/client/message/AbstractJMSMessage.java new file mode 100644 index 0000000000..9cffe2c484 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/AbstractJMSMessage.java @@ -0,0 +1,677 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.AMQException; +import org.apache.qpid.client.AMQDestination; +import org.apache.qpid.client.AMQQueue; +import org.apache.qpid.client.AMQTopic; +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.framing.FieldTable; +import org.apache.qpid.framing.FieldTableKeyEnumeration; +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.commons.lang.NotImplementedException; +import org.apache.mina.common.ByteBuffer; + +import javax.jms.Destination; +import javax.jms.JMSException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; + +public abstract class AbstractJMSMessage extends AMQMessage implements javax.jms.Message +{ + private static final Map _destinationCache = Collections.synchronizedMap(new ReferenceMap()); + +// protected Map _messageProperties; + + public static final char BOOLEAN_PROPERTY_PREFIX = 'B'; + public static final char BYTE_PROPERTY_PREFIX = 'b'; + public static final char SHORT_PROPERTY_PREFIX = 's'; + public static final char INT_PROPERTY_PREFIX = 'i'; + public static final char LONG_PROPERTY_PREFIX = 'l'; + public static final char FLOAT_PROPERTY_PREFIX = 'f'; + public static final char DOUBLE_PROPERTY_PREFIX = 'd'; + public static final char STRING_PROPERTY_PREFIX = 'S'; + + protected boolean _redelivered; + + protected ByteBuffer _data; + + protected AbstractJMSMessage(ByteBuffer data) + { + super(new BasicContentHeaderProperties()); + _data = data; + if (_data != null) + { + _data.acquire(); + } + } + + protected AbstractJMSMessage(long deliveryTag, BasicContentHeaderProperties contentHeader, ByteBuffer data) throws AMQException + { + this(contentHeader, deliveryTag); + _data = data; + if (_data != null) + { + _data.acquire(); + } + } + + protected AbstractJMSMessage(BasicContentHeaderProperties contentHeader, long deliveryTag) + { + super(contentHeader, deliveryTag); + } + + public String getJMSMessageID() throws JMSException + { + if (getJmsContentHeaderProperties().getMessageId() == null) + { + getJmsContentHeaderProperties().setMessageId("ID:" + _deliveryTag); + } + return getJmsContentHeaderProperties().getMessageId(); + } + + public void setJMSMessageID(String messageId) throws JMSException + { + getJmsContentHeaderProperties().setMessageId(messageId); + } + + public long getJMSTimestamp() throws JMSException + { + return new Long(getJmsContentHeaderProperties().getTimestamp()).longValue(); + } + + public void setJMSTimestamp(long timestamp) throws JMSException + { + getJmsContentHeaderProperties().setTimestamp(timestamp); + } + + public byte[] getJMSCorrelationIDAsBytes() throws JMSException + { + return getJmsContentHeaderProperties().getCorrelationId().getBytes(); + } + + public void setJMSCorrelationIDAsBytes(byte[] bytes) throws JMSException + { + getJmsContentHeaderProperties().setCorrelationId(new String(bytes)); + } + + public void setJMSCorrelationID(String correlationId) throws JMSException + { + getJmsContentHeaderProperties().setCorrelationId(correlationId); + } + + public String getJMSCorrelationID() throws JMSException + { + return getJmsContentHeaderProperties().getCorrelationId(); + } + + public Destination getJMSReplyTo() throws JMSException + { + String replyToEncoding = getJmsContentHeaderProperties().getReplyTo(); + if (replyToEncoding == null) + { + return null; + } + else + { + Destination dest = (Destination) _destinationCache.get(replyToEncoding); + if (dest == null) + { + char destType = replyToEncoding.charAt(0); + if (destType == 'Q') + { + dest = new AMQQueue(replyToEncoding.substring(1)); + } + else if (destType == 'T') + { + dest = new AMQTopic(replyToEncoding.substring(1)); + } + else + { + throw new JMSException("Illegal value in JMS_ReplyTo property: " + replyToEncoding); + } + _destinationCache.put(replyToEncoding, dest); + } + return dest; + } + } + + public void setJMSReplyTo(Destination destination) throws JMSException + { + if (destination == null) + { + throw new IllegalArgumentException("Null destination not allowed"); + } + if (!(destination instanceof AMQDestination)) + { + throw new IllegalArgumentException("ReplyTo destination my be an AMQ destination - passed argument was type " + + destination.getClass()); + } + final AMQDestination amqd = (AMQDestination) destination; + + final String encodedDestination = amqd.getEncodedName(); + _destinationCache.put(encodedDestination, destination); + getJmsContentHeaderProperties().setReplyTo(encodedDestination); + } + + public Destination getJMSDestination() throws JMSException + { + // TODO: implement this once we have sorted out how to figure out the exchange class + throw new NotImplementedException(); + } + + public void setJMSDestination(Destination destination) throws JMSException + { + throw new NotImplementedException(); + } + + public int getJMSDeliveryMode() throws JMSException + { + return getJmsContentHeaderProperties().getDeliveryMode(); + } + + public void setJMSDeliveryMode(int i) throws JMSException + { + getJmsContentHeaderProperties().setDeliveryMode((byte) i); + } + + public boolean getJMSRedelivered() throws JMSException + { + return _redelivered; + } + + public void setJMSRedelivered(boolean b) throws JMSException + { + _redelivered = b; + } + + public String getJMSType() throws JMSException + { + return getMimeType(); + } + + public void setJMSType(String string) throws JMSException + { + throw new JMSException("Cannot set JMS Type - it is implicitly defined based on message type"); + } + + public long getJMSExpiration() throws JMSException + { + return new Long(getJmsContentHeaderProperties().getExpiration()).longValue(); + } + + public void setJMSExpiration(long l) throws JMSException + { + getJmsContentHeaderProperties().setExpiration(l); + } + + public int getJMSPriority() throws JMSException + { + return getJmsContentHeaderProperties().getPriority(); + } + + public void setJMSPriority(int i) throws JMSException + { + getJmsContentHeaderProperties().setPriority((byte) i); + } + + public void clearProperties() throws JMSException + { + if (getJmsContentHeaderProperties().getHeaders() != null) + { + getJmsContentHeaderProperties().getHeaders().clear(); + } + } + + public boolean propertyExists(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return false; + } + else + { + // TODO: fix this + return getJmsContentHeaderProperties().getHeaders().containsKey(STRING_PROPERTY_PREFIX + propertyName); + } + } + + public boolean getBooleanProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Boolean.valueOf(null).booleanValue(); + } + else + { + // store as integer as temporary workaround + //Boolean b = (Boolean) getJmsContentHeaderProperties().headers.get(BOOLEAN_PROPERTY_PREFIX + propertyName); + Long b = (Long) getJmsContentHeaderProperties().getHeaders().get(BOOLEAN_PROPERTY_PREFIX + propertyName); + + if (b == null) + { + return Boolean.valueOf(null).booleanValue(); + } + else + { + return b.longValue() != 0; + } + } + } + + public byte getByteProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Byte.valueOf(null).byteValue(); + } + else + { + Byte b = (Byte) getJmsContentHeaderProperties().getHeaders().get(BYTE_PROPERTY_PREFIX + propertyName); + if (b == null) + { + return Byte.valueOf(null).byteValue(); + } + else + { + return b.byteValue(); + } + } + } + + public short getShortProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Short.valueOf(null).shortValue(); + } + else + { + Short s = (Short) getJmsContentHeaderProperties().getHeaders().get(SHORT_PROPERTY_PREFIX + propertyName); + if (s == null) + { + return Short.valueOf(null).shortValue(); + } + else + { + return s.shortValue(); + } + } + } + + public int getIntProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Integer.valueOf(null).intValue(); + } + else + { + Integer i = (Integer) getJmsContentHeaderProperties().getHeaders().get(INT_PROPERTY_PREFIX + propertyName); + if (i == null) + { + return Integer.valueOf(null).intValue(); + } + else + { + return i.intValue(); + } + } + } + + public long getLongProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Long.valueOf(null).longValue(); + } + else + { + Long l = (Long) getJmsContentHeaderProperties().getHeaders().get(LONG_PROPERTY_PREFIX + propertyName); + if (l == null) + { + // temp - the spec says do this but this throws a NumberFormatException + //return Long.valueOf(null).longValue(); + return 0; + } + else + { + return l.longValue(); + } + } + } + + public float getFloatProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Float.valueOf(null).floatValue(); + } + else + { + final Float f = (Float) getJmsContentHeaderProperties().getHeaders().get(FLOAT_PROPERTY_PREFIX + propertyName); + if (f == null) + { + return Float.valueOf(null).floatValue(); + } + else + { + return f.floatValue(); + } + } + } + + public double getDoubleProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return Double.valueOf(null).doubleValue(); + } + else + { + final Double d = (Double) getJmsContentHeaderProperties().getHeaders().get(DOUBLE_PROPERTY_PREFIX + propertyName); + if (d == null) + { + return Double.valueOf(null).doubleValue(); + } + else + { + return d.shortValue(); + } + } + } + + public String getStringProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return null; + } + else + { + return (String) getJmsContentHeaderProperties().getHeaders().get(STRING_PROPERTY_PREFIX + propertyName); + } + } + + public Object getObjectProperty(String propertyName) throws JMSException + { + checkPropertyName(propertyName); + throw new JMSException("Not implemented yet"); + } + + public Enumeration getPropertyNames() throws JMSException + { + return new FieldTableKeyEnumeration(getJmsContentHeaderProperties().getHeaders()) + { + public Object nextElement() + { + String propName = (String) _iterator.next(); + + //The propertyName has a single Char prefix. Skip this. + return propName.substring(1); + } + }; + } + + public void setBooleanProperty(String propertyName, boolean b) throws JMSException + { + checkPropertyName(propertyName); + //getJmsContentHeaderProperties().headers.put(BOOLEAN_PROPERTY_PREFIX + propertyName, Boolean.valueOf(b)); + getJmsContentHeaderProperties().getHeaders().put(BOOLEAN_PROPERTY_PREFIX + propertyName, b ? new Long(1) : new Long(0)); + } + + public void setByteProperty(String propertyName, byte b) throws JMSException + { + checkPropertyName(propertyName); + getJmsContentHeaderProperties().getHeaders().put(BYTE_PROPERTY_PREFIX + propertyName, new Byte(b)); + } + + public void setShortProperty(String propertyName, short i) throws JMSException + { + checkPropertyName(propertyName); + getJmsContentHeaderProperties().getHeaders().put(SHORT_PROPERTY_PREFIX + propertyName, new Short(i)); + } + + public void setIntProperty(String propertyName, int i) throws JMSException + { + checkPropertyName(propertyName); + getJmsContentHeaderProperties().getHeaders().put(INT_PROPERTY_PREFIX + propertyName, new Integer(i)); + } + + public void setLongProperty(String propertyName, long l) throws JMSException + { + checkPropertyName(propertyName); + getJmsContentHeaderProperties().getHeaders().put(LONG_PROPERTY_PREFIX + propertyName, new Long(l)); + } + + public void setFloatProperty(String propertyName, float f) throws JMSException + { + checkPropertyName(propertyName); + getJmsContentHeaderProperties().getHeaders().put(FLOAT_PROPERTY_PREFIX + propertyName, new Float(f)); + } + + public void setDoubleProperty(String propertyName, double v) throws JMSException + { + checkPropertyName(propertyName); + getJmsContentHeaderProperties().getHeaders().put(DOUBLE_PROPERTY_PREFIX + propertyName, new Double(v)); + } + + public void setStringProperty(String propertyName, String value) throws JMSException + { + checkPropertyName(propertyName); + createPropertyMapIfRequired(); + propertyName = STRING_PROPERTY_PREFIX + propertyName; + getJmsContentHeaderProperties().getHeaders().put(propertyName, value); + } + + private void createPropertyMapIfRequired() + { + if (getJmsContentHeaderProperties().getHeaders() == null) + { + getJmsContentHeaderProperties().setHeaders(new FieldTable()); + } + } + + public void setObjectProperty(String string, Object object) throws JMSException + { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void acknowledge() throws JMSException + { + // the JMS 1.1 spec says in section 3.6 that calls to acknowledge are ignored when client acknowledge + // is not specified. In our case, we only set the session field where client acknowledge mode is specified. + if (_session != null) + { + // we set multiple to true here since acknowledgement implies acknowledge of all previous messages + // received on the session + _session.acknowledgeMessage(_deliveryTag, true); + } + } + + public abstract void clearBody() throws JMSException; + + /** + * Get a String representation of the body of the message. Used in the + * toString() method which outputs this before message properties. + */ + public abstract String toBodyString() throws JMSException; + + public abstract String getMimeType(); + + public String toString() + { + try + { + StringBuffer buf = new StringBuffer("Body:\n"); + buf.append(toBodyString()); + buf.append("\nJMS timestamp: ").append(getJMSTimestamp()); + buf.append("\nJMS expiration: ").append(getJMSExpiration()); + buf.append("\nJMS priority: ").append(getJMSPriority()); + buf.append("\nJMS delivery mode: ").append(getJMSDeliveryMode()); + buf.append("\nJMS reply to: ").append(String.valueOf(getJMSReplyTo())); + buf.append("\nAMQ message number: ").append(_deliveryTag); + buf.append("\nProperties:"); + if (getJmsContentHeaderProperties().getHeaders() == null) + { + buf.append("<NONE>"); + } + else + { + final Iterator it = getJmsContentHeaderProperties().getHeaders().entrySet().iterator(); + while (it.hasNext()) + { + final Map.Entry entry = (Map.Entry) it.next(); + final String propertyName = (String) entry.getKey(); + if (propertyName == null) + { + buf.append("\nInternal error: Property with NULL key defined"); + } + else + { + buf.append('\n').append(propertyName.substring(1)); + + char typeIdentifier = propertyName.charAt(0); + switch (typeIdentifier) + { + case org.apache.qpid.client.message.AbstractJMSMessage.BOOLEAN_PROPERTY_PREFIX: + buf.append("<boolean> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.BYTE_PROPERTY_PREFIX: + buf.append("<byte> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.SHORT_PROPERTY_PREFIX: + buf.append("<short> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.INT_PROPERTY_PREFIX: + buf.append("<int> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.LONG_PROPERTY_PREFIX: + buf.append("<long> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.FLOAT_PROPERTY_PREFIX: + buf.append("<float> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.DOUBLE_PROPERTY_PREFIX: + buf.append("<double> "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.STRING_PROPERTY_PREFIX: + buf.append("<string> "); + break; + default: + buf.append("<unknown type (identifier " + + typeIdentifier + ") "); + } + buf.append(String.valueOf(entry.getValue())); + } + } + } + return buf.toString(); + } + catch (JMSException e) + { + return e.toString(); + } + } + + public Map getUnderlyingMessagePropertiesMap() + { + return getJmsContentHeaderProperties().getHeaders(); + } + + public void setUnderlyingMessagePropertiesMap(FieldTable messageProperties) + { + getJmsContentHeaderProperties().setHeaders(messageProperties); + } + + private void checkPropertyName(String propertyName) + { + if (propertyName == null) + { + throw new IllegalArgumentException("Property name must not be null"); + } + else if ("".equals(propertyName)) + { + throw new IllegalArgumentException("Property name must not be the empty string"); + } + + if (getJmsContentHeaderProperties().getHeaders() == null) + { + getJmsContentHeaderProperties().setHeaders(new FieldTable()); + } + } + + public FieldTable populateHeadersFromMessageProperties() + { + if (getJmsContentHeaderProperties().getHeaders() == null) + { + return null; + } + else + { + // + // We need to convert every property into a String representation + // Note that type information is preserved in the property name + // + final FieldTable table = new FieldTable(); + final Iterator entries = getJmsContentHeaderProperties().getHeaders().entrySet().iterator(); + while (entries.hasNext()) + { + final Map.Entry entry = (Map.Entry) entries.next(); + final String propertyName = (String) entry.getKey(); + if (propertyName == null) + { + continue; + } + else + { + table.put(propertyName, entry.getValue().toString()); + } + } + return table; + } + } + + public BasicContentHeaderProperties getJmsContentHeaderProperties() + { + return (BasicContentHeaderProperties) _contentHeaderProperties; + } + + public ByteBuffer getData() + { + // make sure we rewind the data just in case any method has moved the + // position beyond the start + if (_data != null) + { + _data.rewind(); + } + return _data; + } +} diff --git a/java/client/src/org/apache/qpid/client/message/AbstractJMSMessageFactory.java b/java/client/src/org/apache/qpid/client/message/AbstractJMSMessageFactory.java new file mode 100644 index 0000000000..57603f8a7e --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/AbstractJMSMessageFactory.java @@ -0,0 +1,77 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.ContentBody; +import org.apache.qpid.framing.ContentHeaderBody; +import org.apache.log4j.Logger; +import org.apache.mina.common.ByteBuffer; + +import javax.jms.JMSException; +import java.util.Iterator; +import java.util.List; + +public abstract class AbstractJMSMessageFactory implements MessageFactory +{ + private static final Logger _logger = Logger.getLogger(AbstractJMSMessageFactory.class); + + + protected abstract AbstractJMSMessage createMessage(long messageNbr, ByteBuffer data, + ContentHeaderBody contentHeader) throws AMQException; + + protected AbstractJMSMessage createMessageWithBody(long messageNbr, + ContentHeaderBody contentHeader, + List bodies) throws AMQException + { + ByteBuffer data; + + // we optimise the non-fragmented case to avoid copying + if (bodies != null && bodies.size() == 1) + { + _logger.debug("Non-fragmented message body (bodySize=" + contentHeader.bodySize +")"); + data = ((ContentBody)bodies.get(0)).payload; + } + else + { + _logger.debug("Fragmented message body (" + bodies.size() + " frames, bodySize=" + contentHeader.bodySize + ")"); + data = ByteBuffer.allocate((int)contentHeader.bodySize); // XXX: Is cast a problem? + final Iterator it = bodies.iterator(); + while (it.hasNext()) + { + ContentBody cb = (ContentBody) it.next(); + data.put(cb.payload); + cb.payload.release(); + } + data.flip(); + } + _logger.debug("Creating message from buffer with position=" + data.position() + " and remaining=" + data.remaining()); + + return createMessage(messageNbr, data, contentHeader); + } + + public AbstractJMSMessage createMessage(long messageNbr, boolean redelivered, + ContentHeaderBody contentHeader, + List bodies) throws JMSException, AMQException + { + final AbstractJMSMessage msg = createMessageWithBody(messageNbr, contentHeader, bodies); + msg.setJMSRedelivered(redelivered); + return msg; + } + +} diff --git a/java/client/src/org/apache/qpid/client/message/JMSBytesMessage.java b/java/client/src/org/apache/qpid/client/message/JMSBytesMessage.java new file mode 100644 index 0000000000..2cd635f6eb --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/JMSBytesMessage.java @@ -0,0 +1,351 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.framing.ContentHeaderBody; +import org.apache.qpid.AMQException; +import org.apache.mina.common.ByteBuffer; + +import javax.jms.JMSException; +import javax.jms.MessageNotReadableException; +import javax.jms.MessageNotWriteableException; +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.CharacterCodingException; + +public class JMSBytesMessage extends AbstractJMSMessage implements javax.jms.BytesMessage +{ + private static final String MIME_TYPE = "application/octet-stream"; + + private boolean _readable = false; + + /** + * The default initial size of the buffer. The buffer expands automatically. + */ + private static final int DEFAULT_BUFFER_INITIAL_SIZE = 1024; + + JMSBytesMessage() + { + this(null); + } + + /** + * Construct a bytes message with existing data. + * @param data the data that comprises this message. If data is null, you get a 1024 byte buffer that is + * set to auto expand + */ + JMSBytesMessage(ByteBuffer data) + { + super(data); // this instanties a content header + getJmsContentHeaderProperties().setContentType(MIME_TYPE); + + if (_data == null) + { + _data = ByteBuffer.allocate(DEFAULT_BUFFER_INITIAL_SIZE); + _data.setAutoExpand(true); + } + _readable = (data != null); + } + + JMSBytesMessage(long messageNbr, ByteBuffer data, ContentHeaderBody contentHeader) + throws AMQException + { + // TODO: this casting is ugly. Need to review whole ContentHeaderBody idea + super(messageNbr, (BasicContentHeaderProperties) contentHeader.properties, data); + getJmsContentHeaderProperties().setContentType(MIME_TYPE); + _readable = true; + } + + public void clearBody() throws JMSException + { + _data.clear(); + _readable = false; + } + + public String toBodyString() throws JMSException + { + checkReadable(); + try + { + return getText(); + } + catch (IOException e) + { + throw new JMSException(e.toString()); + } + } + + /** + * We reset the stream before and after reading the data. This means that toString() will always output + * the entire message and also that the caller can then immediately start reading as if toString() had + * never been called. + * @return + * @throws IOException + */ + private String getText() throws IOException + { + // this will use the default platform encoding + if (_data == null) + { + return null; + } + int pos = _data.position(); + _data.rewind(); + // one byte left is for the end of frame marker + if (_data.remaining() == 0) + { + // this is really redundant since pos must be zero + _data.position(pos); + return null; + } + else + { + String data = _data.getString(Charset.forName("UTF8").newDecoder()); + _data.position(pos); + return data; + } + } + + public String getMimeType() + { + return MIME_TYPE; + } + + public long getBodyLength() throws JMSException + { + checkReadable(); + return _data.limit(); + } + + private void checkReadable() throws MessageNotReadableException + { + if (!_readable) + { + throw new MessageNotReadableException("You need to call reset() to make the message readable"); + } + } + + private void checkWritable() throws MessageNotWriteableException + { + if (_readable) + { + throw new MessageNotWriteableException("You need to call clearBody() to make the message writable"); + } + } + + public boolean readBoolean() throws JMSException + { + checkReadable(); + return _data.get() != 0; + } + + public byte readByte() throws JMSException + { + checkReadable(); + return _data.get(); + } + + public int readUnsignedByte() throws JMSException + { + checkReadable(); + return _data.getUnsigned(); + } + + public short readShort() throws JMSException + { + checkReadable(); + return _data.getShort(); + } + + public int readUnsignedShort() throws JMSException + { + checkReadable(); + return _data.getUnsignedShort(); + } + + public char readChar() throws JMSException + { + checkReadable(); + return _data.getChar(); + } + + public int readInt() throws JMSException + { + checkReadable(); + return _data.getInt(); + } + + public long readLong() throws JMSException + { + checkReadable(); + return _data.getLong(); + } + + public float readFloat() throws JMSException + { + checkReadable(); + return _data.getFloat(); + } + + public double readDouble() throws JMSException + { + checkReadable(); + return _data.getDouble(); + } + + public String readUTF() throws JMSException + { + checkReadable(); + try + { + return _data.getString(Charset.forName("UTF-8").newDecoder()); + } + catch (CharacterCodingException e) + { + JMSException je = new JMSException("Error decoding byte stream as a UTF8 string: " + e); + je.setLinkedException(e); + throw je; + } + } + + public int readBytes(byte[] bytes) throws JMSException + { + if (bytes == null) + { + throw new IllegalArgumentException("byte array must not be null"); + } + checkReadable(); + int count = (_data.remaining() >= bytes.length ? bytes.length : _data.remaining()); + _data.get(bytes, 0, count); + return count; + } + + public int readBytes(byte[] bytes, int maxLength) throws JMSException + { + if (bytes == null) + { + throw new IllegalArgumentException("byte array must not be null"); + } + if (maxLength > bytes.length) + { + throw new IllegalArgumentException("maxLength must be <= bytes.length"); + } + checkReadable(); + int count = (_data.remaining() >= maxLength ? maxLength : _data.remaining()); + _data.get(bytes, 0, count); + return count; + } + + public void writeBoolean(boolean b) throws JMSException + { + checkWritable(); + _data.put(b?(byte)1:(byte)0); + } + + public void writeByte(byte b) throws JMSException + { + checkWritable(); + _data.put(b); + } + + public void writeShort(short i) throws JMSException + { + checkWritable(); + _data.putShort(i); + } + + public void writeChar(char c) throws JMSException + { + checkWritable(); + _data.putChar(c); + } + + public void writeInt(int i) throws JMSException + { + checkWritable(); + _data.putInt(i); + } + + public void writeLong(long l) throws JMSException + { + checkWritable(); + _data.putLong(l); + } + + public void writeFloat(float v) throws JMSException + { + checkWritable(); + _data.putFloat(v); + } + + public void writeDouble(double v) throws JMSException + { + checkWritable(); + _data.putDouble(v); + } + + public void writeUTF(String string) throws JMSException + { + checkWritable(); + try + { + _data.putString(string, Charset.forName("UTF-8").newEncoder()); + } + catch (CharacterCodingException e) + { + JMSException ex = new JMSException("Unable to encode string: " + e); + ex.setLinkedException(e); + throw ex; + } + } + + public void writeBytes(byte[] bytes) throws JMSException + { + checkWritable(); + _data.put(bytes); + } + + public void writeBytes(byte[] bytes, int offset, int length) throws JMSException + { + checkWritable(); + _data.put(bytes, offset, length); + } + + public void writeObject(Object object) throws JMSException + { + checkWritable(); + if (object == null) + { + throw new NullPointerException("Argument must not be null"); + } + _data.putObject(object); + } + + public void reset() throws JMSException + { + //checkWritable(); + _data.flip(); + _readable = true; + } + + public boolean isReadable() + { + return _readable; + } +} diff --git a/java/client/src/org/apache/qpid/client/message/JMSBytesMessageFactory.java b/java/client/src/org/apache/qpid/client/message/JMSBytesMessageFactory.java new file mode 100644 index 0000000000..0e3aa45047 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/JMSBytesMessageFactory.java @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.mina.common.ByteBuffer; +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.ContentHeaderBody; + +import javax.jms.JMSException; + +public class JMSBytesMessageFactory extends AbstractJMSMessageFactory +{ + protected AbstractJMSMessage createMessage(long deliveryTag, ByteBuffer data, ContentHeaderBody contentHeader) throws AMQException + { + return new JMSBytesMessage(deliveryTag, data, contentHeader); + } + + public AbstractJMSMessage createMessage() throws JMSException + { + return new JMSBytesMessage(); + } +} diff --git a/java/client/src/org/apache/qpid/client/message/JMSObjectMessage.java b/java/client/src/org/apache/qpid/client/message/JMSObjectMessage.java new file mode 100644 index 0000000000..f0ac87c2d7 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/JMSObjectMessage.java @@ -0,0 +1,175 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.framing.ContentHeaderBody; +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.AMQException; +import org.apache.mina.common.ByteBuffer; + +import javax.jms.ObjectMessage; +import javax.jms.JMSException; +import javax.jms.MessageFormatException; +import javax.jms.MessageNotWriteableException; +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.CharacterCodingException; + +public class JMSObjectMessage extends AbstractJMSMessage implements ObjectMessage +{ + static final String MIME_TYPE = "application/java-object-stream"; + private final boolean _readonly; + + private static final int DEFAULT_BUFFER_SIZE = 1024; + /** + * Creates empty, writable message for use by producers + */ + JMSObjectMessage() + { + this(null); + } + + private JMSObjectMessage(ByteBuffer data) + { + super(data); + if (data == null) + { + _data = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + _data.setAutoExpand(true); + } + _readonly = (data != null); + getJmsContentHeaderProperties().setContentType(MIME_TYPE); + } + + /** + * Creates read only message for delivery to consumers + */ + JMSObjectMessage(long messageNbr, ByteBuffer data, ContentHeaderBody contentHeader) throws AMQException + { + super(messageNbr, (BasicContentHeaderProperties) contentHeader.properties, data); + _readonly = data != null; + } + + public void clearBody() throws JMSException + { + if (_data != null) + { + _data.release(); + } + _data = null; + } + + public String toBodyString() throws JMSException + { + return toString(_data); + } + + public String getMimeType() + { + return MIME_TYPE; + } + + public void setObject(Serializable serializable) throws JMSException + { + if (_readonly) + { + throw new MessageNotWriteableException("Message is not writable."); + } + + if (_data == null) + { + _data = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + _data.setAutoExpand(true); + } + try + { + ObjectOutputStream out = new ObjectOutputStream(_data.asOutputStream()); + out.writeObject(serializable); + out.flush(); + out.close(); + _data.rewind(); + } + catch (IOException e) + { + throw new MessageFormatException("Message not serializable: " + e); + } + } + + public Serializable getObject() throws JMSException + { + ObjectInputStream in = null; + if (_data == null) + { + return null; + } + + try + { + in = new ObjectInputStream(_data.asInputStream()); + return (Serializable) in.readObject(); + } + catch (IOException e) + { + throw new MessageFormatException("Could not deserialize message: " + e); + } + catch (ClassNotFoundException e) + { + throw new MessageFormatException("Could not deserialize message: " + e); + } + finally + { + _data.rewind(); + close(in); + } + } + + private static void close(InputStream in) + { + try + { + if (in != null) + { + in.close(); + } + } + catch (IOException ignore) + { + } + } + + private static String toString(ByteBuffer data) + { + if (data == null) + { + return null; + } + int pos = data.position(); + try + { + return data.getString(Charset.forName("UTF8").newDecoder()); + } + catch (CharacterCodingException e) + { + return null; + } + finally + { + data.position(pos); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/message/JMSObjectMessageFactory.java b/java/client/src/org/apache/qpid/client/message/JMSObjectMessageFactory.java new file mode 100644 index 0000000000..cb5f29ec74 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/JMSObjectMessageFactory.java @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.mina.common.ByteBuffer; +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.ContentHeaderBody; + +import javax.jms.JMSException; + +public class JMSObjectMessageFactory extends AbstractJMSMessageFactory +{ + protected AbstractJMSMessage createMessage(long deliveryTag, ByteBuffer data, ContentHeaderBody contentHeader) throws AMQException + { + return new JMSObjectMessage(deliveryTag, data, contentHeader); + } + + public AbstractJMSMessage createMessage() throws JMSException + { + return new JMSObjectMessage(); + } +} diff --git a/java/client/src/org/apache/qpid/client/message/JMSTextMessage.java b/java/client/src/org/apache/qpid/client/message/JMSTextMessage.java new file mode 100644 index 0000000000..b860d55e94 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/JMSTextMessage.java @@ -0,0 +1,159 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.AMQException; +import org.apache.mina.common.ByteBuffer; + +import javax.jms.JMSException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharacterCodingException; + +public class JMSTextMessage extends AbstractJMSMessage implements javax.jms.TextMessage +{ + private static final String MIME_TYPE = "text/plain"; + + private String _decodedValue; + + JMSTextMessage() throws JMSException + { + this(null, null); + } + + JMSTextMessage(ByteBuffer data, String encoding) throws JMSException + { + super(data); // this instantiates a content header + getJmsContentHeaderProperties().setContentType(MIME_TYPE); + getJmsContentHeaderProperties().setEncoding(encoding); + } + + JMSTextMessage(long deliveryTag, ByteBuffer data, BasicContentHeaderProperties contentHeader) + throws AMQException + { + super(deliveryTag, contentHeader, data); + contentHeader.setContentType(MIME_TYPE); + _data = data; + } + + JMSTextMessage(ByteBuffer data) throws JMSException + { + this(data, null); + } + + JMSTextMessage(String text) throws JMSException + { + super((ByteBuffer)null); + setText(text); + } + + public void clearBody() throws JMSException + { + if (_data != null) + { + _data.release(); + } + _data = null; + _decodedValue = null; + } + + public String toBodyString() throws JMSException + { + return getText(); + } + + public void setData(ByteBuffer data) + { + _data = data; + } + + public String getMimeType() + { + return MIME_TYPE; + } + + public void setText(String string) throws JMSException + { + clearBody(); + try + { + _data = ByteBuffer.allocate(string.length()); + _data.limit(string.length()); + //_data.sweep(); + _data.setAutoExpand(true); + if (getJmsContentHeaderProperties().getEncoding() == null) + { + _data.put(string.getBytes()); + } + else + { + _data.put(string.getBytes(getJmsContentHeaderProperties().getEncoding())); + } + _decodedValue = string; + } + catch (UnsupportedEncodingException e) + { + // should never occur + throw new JMSException("Unable to decode string data"); + } + } + + public String getText() throws JMSException + { + if (_data == null && _decodedValue == null) + { + return null; + } + else if (_decodedValue != null) + { + return _decodedValue; + } + else + { + _data.rewind(); + if (getJmsContentHeaderProperties().getEncoding() != null) + { + try + { + _decodedValue = _data.getString(Charset.forName(getJmsContentHeaderProperties().getEncoding()).newDecoder()); + } + catch (CharacterCodingException e) + { + JMSException je = new JMSException("Could not decode string data: " + e); + je.setLinkedException(e); + throw je; + } + } + else + { + try + { + _decodedValue = _data.getString(Charset.defaultCharset().newDecoder()); + } + catch (CharacterCodingException e) + { + JMSException je = new JMSException("Could not decode string data: " + e); + je.setLinkedException(e); + throw je; + } + } + return _decodedValue; + } + } +} diff --git a/java/client/src/org/apache/qpid/client/message/JMSTextMessageFactory.java b/java/client/src/org/apache/qpid/client/message/JMSTextMessageFactory.java new file mode 100644 index 0000000000..747965d26e --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/JMSTextMessageFactory.java @@ -0,0 +1,39 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.mina.common.ByteBuffer; +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.framing.ContentHeaderBody; + +import javax.jms.JMSException; + +public class JMSTextMessageFactory extends AbstractJMSMessageFactory +{ + + public AbstractJMSMessage createMessage() throws JMSException + { + return new JMSTextMessage(); + } + + protected AbstractJMSMessage createMessage(long deliveryTag, ByteBuffer data, ContentHeaderBody contentHeader) throws AMQException + { + return new JMSTextMessage(deliveryTag, data, (BasicContentHeaderProperties)contentHeader.properties); + } +} diff --git a/java/client/src/org/apache/qpid/client/message/MessageFactory.java b/java/client/src/org/apache/qpid/client/message/MessageFactory.java new file mode 100644 index 0000000000..a9c7dd1b48 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/MessageFactory.java @@ -0,0 +1,35 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.ContentHeaderBody; + +import javax.jms.JMSException; +import java.util.List; + + +public interface MessageFactory +{ + AbstractJMSMessage createMessage(long deliveryTag, boolean redelivered, + ContentHeaderBody contentHeader, + List bodies) + throws JMSException, AMQException; + + AbstractJMSMessage createMessage() throws JMSException; +} diff --git a/java/client/src/org/apache/qpid/client/message/MessageFactoryRegistry.java b/java/client/src/org/apache/qpid/client/message/MessageFactoryRegistry.java new file mode 100644 index 0000000000..5808f9aa35 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/MessageFactoryRegistry.java @@ -0,0 +1,105 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.framing.ContentHeaderBody; + +import javax.jms.JMSException; +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +public class MessageFactoryRegistry +{ + private final Map _mimeToFactoryMap = new HashMap(); + + public void registerFactory(String mimeType, MessageFactory mf) + { + if (mf == null) + { + throw new IllegalArgumentException("Message factory must not be null"); + } + _mimeToFactoryMap.put(mimeType, mf); + } + + public MessageFactory deregisterFactory(String mimeType) + { + return (MessageFactory) _mimeToFactoryMap.remove(mimeType); + } + + /** + * Create a message. This looks up the MIME type from the content header and instantiates the appropriate + * concrete message type. + * @param deliveryTag the AMQ message id + * @param redelivered true if redelivered + * @param contentHeader the content header that was received + * @param bodies a list of ContentBody instances + * @return the message. + * @throws AMQException + * @throws JMSException + */ + public AbstractJMSMessage createMessage(long deliveryTag, boolean redelivered, + ContentHeaderBody contentHeader, + List bodies) throws AMQException, JMSException + { + BasicContentHeaderProperties properties = (BasicContentHeaderProperties) contentHeader.properties; + MessageFactory mf = (MessageFactory) _mimeToFactoryMap.get(properties.getContentType()); + if (mf == null) + { + throw new AMQException("Unsupport MIME type of " + properties.getContentType()); + } + else + { + return mf.createMessage(deliveryTag, redelivered, contentHeader, bodies); + } + } + + public AbstractJMSMessage createMessage(String mimeType) throws AMQException, JMSException + { + if (mimeType == null) + { + throw new IllegalArgumentException("Mime type must not be null"); + } + MessageFactory mf = (MessageFactory) _mimeToFactoryMap.get(mimeType); + if (mf == null) + { + throw new AMQException("Unsupport MIME type of " + mimeType); + } + else + { + return mf.createMessage(); + } + } + + /** + * Construct a new registry with the default message factories registered + * @return a message factory registry + */ + public static MessageFactoryRegistry newDefaultRegistry() + { + MessageFactoryRegistry mf = new MessageFactoryRegistry(); + mf.registerFactory("text/plain", new JMSTextMessageFactory()); + mf.registerFactory("text/xml", new JMSTextMessageFactory()); + mf.registerFactory("application/octet-stream", new JMSBytesMessageFactory()); + mf.registerFactory(JMSObjectMessage.MIME_TYPE, new JMSObjectMessageFactory()); + mf.registerFactory(null, new JMSBytesMessageFactory()); + return mf; + } +} diff --git a/java/client/src/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java b/java/client/src/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java new file mode 100644 index 0000000000..f2cb7defd2 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java @@ -0,0 +1,40 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.AMQException; +import org.apache.log4j.Logger; + +public class UnexpectedBodyReceivedException extends AMQException +{ + + public UnexpectedBodyReceivedException(Logger logger, String msg, Throwable t) + { + super(logger, msg, t); + } + + public UnexpectedBodyReceivedException(Logger logger, String msg) + { + super(logger, msg); + } + + public UnexpectedBodyReceivedException(Logger logger, int errorCode, String msg) + { + super(logger, errorCode, msg); + } +} diff --git a/java/client/src/org/apache/qpid/client/message/UnprocessedMessage.java b/java/client/src/org/apache/qpid/client/message/UnprocessedMessage.java new file mode 100644 index 0000000000..7d4ee097d3 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/message/UnprocessedMessage.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.message; + +import org.apache.qpid.framing.*; + +import java.util.List; +import java.util.LinkedList; + +/** + * This class contains everything needed to process a JMS message. It assembles the + * deliver body, the content header and the content body/ies. + * + * Note that the actual work of creating a JMS message for the client code's use is done + * outside of the MINA dispatcher thread in order to minimise the amount of work done in + * the MINA dispatcher thread. + * + */ +public class UnprocessedMessage +{ + private long _bytesReceived = 0; + + public BasicDeliverBody deliverBody; + public BasicReturnBody bounceBody; // TODO: check change (gustavo) + public int channelId; + public ContentHeaderBody contentHeader; + + /** + * List of ContentBody instances. Due to fragmentation you don't know how big this will be in general + */ + public List bodies = new LinkedList(); + + public void receiveBody(ContentBody body) throws UnexpectedBodyReceivedException + { + bodies.add(body); + if (body.payload != null) + { + _bytesReceived += body.payload.remaining(); + } + } + + public boolean isAllBodyDataReceived() + { + return _bytesReceived == contentHeader.bodySize; + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/AMQMethodEvent.java b/java/client/src/org/apache/qpid/client/protocol/AMQMethodEvent.java new file mode 100644 index 0000000000..b14a7cb8b0 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/AMQMethodEvent.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.qpid.framing.AMQMethodBody; + +public class AMQMethodEvent +{ + private AMQMethodBody _method; + + private int _channelId; + + private AMQProtocolSession _protocolSession; + + public AMQMethodEvent(int channelId, AMQMethodBody method, AMQProtocolSession protocolSession) + { + _channelId = channelId; + _method = method; + _protocolSession = protocolSession; + } + + public AMQMethodBody getMethod() + { + return _method; + } + + public int getChannelId() + { + return _channelId; + } + + public AMQProtocolSession getProtocolSession() + { + return _protocolSession; + } + + public String toString() + { + StringBuffer buf = new StringBuffer("Method event: "); + buf.append("\nChannel id: ").append(_channelId); + buf.append("\nMethod: ").append(_method); + return buf.toString(); + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/AMQMethodListener.java b/java/client/src/org/apache/qpid/client/protocol/AMQMethodListener.java new file mode 100644 index 0000000000..fd20151e95 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/AMQMethodListener.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.qpid.AMQException; + +public interface AMQMethodListener +{ + /** + * Invoked when a method frame has been received + * @param evt the event + * @return true if the handler has processed the method frame, false otherwise. Note + * that this does not prohibit the method event being delivered to subsequent listeners + * but can be used to determine if nobody has dealt with an incoming method frame. + * @throws AMQException if an error has occurred. This exception will be delivered + * to all registered listeners using the error() method (see below) allowing them to + * perform cleanup if necessary. + */ + boolean methodReceived(AMQMethodEvent evt) throws AMQException; + + /** + * Callback when an error has occurred. Allows listeners to clean up. + * @param e + */ + void error(Exception e); +} diff --git a/java/client/src/org/apache/qpid/client/protocol/AMQProtocolHandler.java b/java/client/src/org/apache/qpid/client/protocol/AMQProtocolHandler.java new file mode 100644 index 0000000000..0c4dbcbb26 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/AMQProtocolHandler.java @@ -0,0 +1,530 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.log4j.Logger; +import org.apache.mina.common.IdleStatus; +import org.apache.mina.common.IoHandlerAdapter; +import org.apache.mina.common.IoSession; +import org.apache.mina.filter.SSLFilter; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.qpid.AMQDisconnectedException; +import org.apache.qpid.AMQException; + +import org.apache.qpid.AMQConnectionClosedException; +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.client.AMQSession; +import org.apache.qpid.client.failover.FailoverHandler; +import org.apache.qpid.client.failover.FailoverState; + +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.state.AMQStateManager; +import org.apache.qpid.client.state.listener.SpecificMethodFrameListener; +import org.apache.qpid.codec.AMQCodecFactory; +import org.apache.qpid.framing.*; +import org.apache.qpid.protocol.AMQConstant; +import org.apache.qpid.ssl.BogusSSLContextFactory; + +import java.util.Iterator; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.CountDownLatch; + +public class AMQProtocolHandler extends IoHandlerAdapter +{ + private static final Logger _logger = Logger.getLogger(AMQProtocolHandler.class); + + /** + * The connection that this protocol handler is associated with. There is a 1-1 + * mapping between connection instances and protocol handler instances. + */ + private AMQConnection _connection; + + /** + * Used only when determining whether to add the SSL filter or not. This should be made more + * generic in future since we will potentially have many transport layer options + */ + private boolean _useSSL; + + /** + * Our wrapper for a protocol session that provides access to session values + * in a typesafe manner. + */ + private volatile AMQProtocolSession _protocolSession; + + private AMQStateManager _stateManager = new AMQStateManager(); + + private final CopyOnWriteArraySet _frameListeners = new CopyOnWriteArraySet(); + + /** + * We create the failover handler when the session is created since it needs a reference to the IoSession in order + * to be able to send errors during failover back to the client application. The session won't be available in the + * case where we failing over due to a Connection.Redirect message from the broker. + */ + private FailoverHandler _failoverHandler; + + /** + * This flag is used to track whether failover is being attempted. It is used to prevent the application constantly + * attempting failover where it is failing. + */ + private FailoverState _failoverState = FailoverState.NOT_STARTED; + + private CountDownLatch _failoverLatch; + + public AMQProtocolHandler(AMQConnection con) + { + _connection = con; + + // We add a proxy for the state manager so that we can substitute the state manager easily in this class. + // We substitute the state manager when performing failover + _frameListeners.add(new AMQMethodListener() + { + public boolean methodReceived(AMQMethodEvent evt) throws AMQException + { + return _stateManager.methodReceived(evt); + } + + public void error(Exception e) + { + _stateManager.error(e); + } + }); + } + + public boolean isUseSSL() + { + return _useSSL; + } + + public void setUseSSL(boolean useSSL) + { + _useSSL = useSSL; + } + + public void sessionCreated(IoSession session) throws Exception + { + _logger.debug("Protocol session created for session " + System.identityHashCode(session)); + _failoverHandler = new FailoverHandler(this, session); + + final ProtocolCodecFilter pcf = new ProtocolCodecFilter(new AMQCodecFactory(false)); + + if (Boolean.getBoolean("amqj.shared_read_write_pool")) + { + session.getFilterChain().addBefore("AsynchronousWriteFilter", "protocolFilter", pcf); + } + else + { + session.getFilterChain().addLast("protocolFilter", pcf); + } + // we only add the SSL filter where we have an SSL connection + if (_useSSL) + { + //todo FIXME: Bogus context cannot be used in production. + SSLFilter sslFilter = new SSLFilter(BogusSSLContextFactory.getInstance(false)); + sslFilter.setUseClientMode(true); + session.getFilterChain().addBefore("protocolFilter", "ssl", sslFilter); + } + + _protocolSession = new AMQProtocolSession(this, session, _connection); + _protocolSession.init(); + } + + public void sessionOpened(IoSession session) throws Exception + { + System.setProperty("foo", "bar"); + } + + /** + * When the broker connection dies we can either get sessionClosed() called or exceptionCaught() followed by + * sessionClosed() depending on whether we were trying to send data at the time of failure. + * + * @param session + * @throws Exception + */ + public void sessionClosed(IoSession session) throws Exception + { + //todo server just closes session with no warning if auth fails. + if (_connection.isClosed()) + { + _logger.info("Session closed called by client"); + } + else + { + _logger.info("Session closed called with failover state currently " + _failoverState); + + //reconnetablility was introduced here so as not to disturb the client as they have made their intentions + // known through the policy settings. + + if ((_failoverState != FailoverState.IN_PROGRESS) && _connection.failoverAllowed()) + { + _logger.info("FAILOVER STARTING"); + if (_failoverState == FailoverState.NOT_STARTED) + { + _failoverState = FailoverState.IN_PROGRESS; + startFailoverThread(); + } + else + { + _logger.info("Not starting failover as state currently " + _failoverState); + } + } + else + { + _logger.info("Failover not allowed by policy."); + + if (_failoverState != FailoverState.IN_PROGRESS) + { + _logger.info("sessionClose() not allowed to failover"); + _connection.exceptionReceived( + new AMQDisconnectedException("Server closed connection and reconnection " + + "not permitted.")); + } + else + { + _logger.info("sessionClose() failover in progress"); + } + } + } + + _logger.info("Protocol Session [" + this + "] closed"); + } + + /** + * See {@link FailoverHandler} to see rationale for separate thread. + */ + private void startFailoverThread() + { + Thread failoverThread = new Thread(_failoverHandler); + failoverThread.setName("Failover"); + // Do not inherit daemon-ness from current thread as this can be a daemon + // thread such as a AnonymousIoService thread. + failoverThread.setDaemon(false); + failoverThread.start(); + } + + public void sessionIdle(IoSession session, IdleStatus status) throws Exception + { + _logger.debug("Protocol Session [" + this + ":" + session + "] idle: " + status); + if (IdleStatus.WRITER_IDLE.equals(status)) + { + //write heartbeat frame: + _logger.debug("Sent heartbeat"); + session.write(HeartbeatBody.FRAME); + HeartbeatDiagnostics.sent(); + } + else if (IdleStatus.READER_IDLE.equals(status)) + { + //failover: + HeartbeatDiagnostics.timeout(); + _logger.warn("Timed out while waiting for heartbeat from peer."); + session.close(); + } + } + + public void exceptionCaught(IoSession session, Throwable cause) throws Exception + { + if (_failoverState == FailoverState.NOT_STARTED) + { + //if (!(cause instanceof AMQUndeliveredException) && (!(cause instanceof AMQAuthenticationException))) + if (cause instanceof AMQConnectionClosedException) + { + _logger.info("Exception caught therefore going to attempt failover: " + cause, cause); + // this will attemp failover + + sessionClosed(session); + } + } + // we reach this point if failover was attempted and failed therefore we need to let the calling app + // know since we cannot recover the situation + else if (_failoverState == FailoverState.FAILED) + { + _logger.error("Exception caught by protocol handler: " + cause, cause); + // we notify the state manager of the error in case we have any clients waiting on a state + // change. Those "waiters" will be interrupted and can handle the exception + AMQException amqe = new AMQException("Protocol handler error: " + cause, cause); + propagateExceptionToWaiters(amqe); + _connection.exceptionReceived(cause); + } + } + + /** + * There are two cases where we have other threads potentially blocking for events to be handled by this + * class. These are for the state manager (waiting for a state change) or a frame listener (waiting for a + * particular type of frame to arrive). When an error occurs we need to notify these waiters so that they can + * react appropriately. + * + * @param e the exception to propagate + */ + public void propagateExceptionToWaiters(Exception e) + { + _stateManager.error(e); + final Iterator it = _frameListeners.iterator(); + while (it.hasNext()) + { + final AMQMethodListener ml = (AMQMethodListener) it.next(); + ml.error(e); + } + } + + private static int _messageReceivedCount; + + public void messageReceived(IoSession session, Object message) throws Exception + { + + if (_messageReceivedCount++ % 1000 == 0) + { + _logger.debug("Received " + _messageReceivedCount + " protocol messages"); + } + Iterator it = _frameListeners.iterator(); + AMQFrame frame = (AMQFrame) message; + + HeartbeatDiagnostics.received(frame.bodyFrame instanceof HeartbeatBody); + + if (frame.bodyFrame instanceof AMQMethodBody) + { + if (_logger.isDebugEnabled()) + { + _logger.debug("Method frame received: " + frame); + } + + final AMQMethodEvent evt = new AMQMethodEvent(frame.channel, (AMQMethodBody)frame.bodyFrame, _protocolSession); + try + { + boolean wasAnyoneInterested = false; + while (it.hasNext()) + { + final AMQMethodListener listener = (AMQMethodListener) it.next(); + wasAnyoneInterested = listener.methodReceived(evt) || wasAnyoneInterested; + } + if (!wasAnyoneInterested) + { + throw new AMQException("AMQMethodEvent " + evt + " was not processed by any listener."); + } + } + catch (AMQException e) + { + it = _frameListeners.iterator(); + while (it.hasNext()) + { + final AMQMethodListener listener = (AMQMethodListener) it.next(); + listener.error(e); + } + exceptionCaught(session, e); + } + } + else if (frame.bodyFrame instanceof ContentHeaderBody) + { + _protocolSession.messageContentHeaderReceived(frame.channel, + (ContentHeaderBody) frame.bodyFrame); + } + else if (frame.bodyFrame instanceof ContentBody) + { + _protocolSession.messageContentBodyReceived(frame.channel, + (ContentBody) frame.bodyFrame); + } + else if (frame.bodyFrame instanceof HeartbeatBody) + { + _logger.debug("Received heartbeat"); + } + _connection.bytesReceived(_protocolSession.getIoSession().getReadBytes()); + } + + private static int _messagesOut; + + public void messageSent(IoSession session, Object message) throws Exception + { + if (_messagesOut++ % 1000 == 0) + { + _logger.debug("Sent " + _messagesOut + " protocol messages"); + } + _connection.bytesSent(session.getWrittenBytes()); + if (_logger.isDebugEnabled()) + { + _logger.debug("Sent frame " + message); + } + } + + public void addFrameListener(AMQMethodListener listener) + { + _frameListeners.add(listener); + } + + public void removeFrameListener(AMQMethodListener listener) + { + _frameListeners.remove(listener); + } + + public void attainState(AMQState s) throws AMQException + { + _stateManager.attainState(s); + } + + /** + * 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) + { + _protocolSession.writeFrame(frame); + } + + public void writeFrame(AMQDataBlock frame, boolean wait) + { + _protocolSession.writeFrame(frame, wait); + } + + /** + * Convenience method that writes a frame to the protocol session and waits for + * a particular response. Equivalent to calling getProtocolSession().write() then + * waiting for the response. + * + * @param frame + * @param listener the blocking listener. Note the calling thread will block. + */ + private AMQMethodEvent writeCommandFrameAndWaitForReply(AMQFrame frame, + BlockingMethodFrameListener listener) + throws AMQException + { + _frameListeners.add(listener); + _protocolSession.writeFrame(frame); + return listener.blockForFrame(); + // When control resumes before this line, a reply will have been received + // that matches the criteria defined in the blocking listener + } + + /** + * More convenient method to write a frame and wait for it's response. + */ + public AMQMethodEvent syncWrite(AMQFrame frame, Class responseClass) throws AMQException + { + return writeCommandFrameAndWaitForReply(frame, + new SpecificMethodFrameListener(frame.channel, responseClass)); + } + + /** + * Convenience method to register an AMQSession with the protocol handler. Registering + * a session with the protocol handler will ensure that messages are delivered to the + * consumer(s) on that session. + * + * @param channelId the channel id of the session + * @param session the session instance. + */ + public void addSessionByChannel(int channelId, AMQSession session) + { + _protocolSession.addSessionByChannel(channelId, session); + } + + /** + * Convenience method to deregister an AMQSession with the protocol handler. + * + * @param channelId then channel id of the session + */ + public void removeSessionByChannel(int channelId) + { + _protocolSession.removeSessionByChannel(channelId); + } + + public void closeSession(AMQSession session) throws AMQException + { + _protocolSession.closeSession(session); + } + + public void closeConnection() throws AMQException + { + _stateManager.changeState(AMQState.CONNECTION_CLOSING); + + final AMQFrame frame = ConnectionCloseBody.createAMQFrame( + 0, AMQConstant.REPLY_SUCCESS.getCode(), "JMS client is closing the connection.", 0, 0); + syncWrite(frame, ConnectionCloseOkBody.class); + + _protocolSession.closeProtocolSession(); + } + + /** + * @return the number of bytes read from this protocol session + */ + public long getReadBytes() + { + return _protocolSession.getIoSession().getReadBytes(); + } + + /** + * @return the number of bytes written to this protocol session + */ + public long getWrittenBytes() + { + return _protocolSession.getIoSession().getWrittenBytes(); + } + + public void failover(String host, int port) + { + _failoverHandler.setHost(host); + _failoverHandler.setPort(port); + // see javadoc for FailoverHandler to see rationale for separate thread + startFailoverThread(); + } + + public void blockUntilNotFailingOver() throws InterruptedException + { + if (_failoverLatch != null) + { + _failoverLatch.await(); + } + } + + public String generateQueueName() + { + return _protocolSession.generateQueueName(); + } + + public CountDownLatch getFailoverLatch() + { + return _failoverLatch; + } + + public void setFailoverLatch(CountDownLatch failoverLatch) + { + _failoverLatch = failoverLatch; + } + + public AMQConnection getConnection() + { + return _connection; + } + + public AMQStateManager getStateManager() + { + return _stateManager; + } + + public void setStateManager(AMQStateManager stateManager) + { + _stateManager = stateManager; + } + + FailoverState getFailoverState() + { + return _failoverState; + } + + public void setFailoverState(FailoverState failoverState) + { + _failoverState = failoverState; + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/AMQProtocolSession.java b/java/client/src/org/apache/qpid/client/protocol/AMQProtocolSession.java new file mode 100644 index 0000000000..77685a0222 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/AMQProtocolSession.java @@ -0,0 +1,379 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.log4j.Logger; +import org.apache.mina.common.CloseFuture; +import org.apache.mina.common.IdleStatus; +import org.apache.mina.common.IoSession; +import org.apache.mina.common.WriteFuture; +import org.apache.qpid.AMQException; +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.client.AMQSession; +import org.apache.qpid.client.ConnectionTuneParameters; +import org.apache.qpid.client.message.UnexpectedBodyReceivedException; +import org.apache.qpid.client.message.UnprocessedMessage; +import org.apache.qpid.framing.AMQDataBlock; +import org.apache.qpid.framing.ContentBody; +import org.apache.qpid.framing.ContentHeaderBody; +import org.apache.qpid.framing.ProtocolInitiation; +import org.apache.qpid.framing.ProtocolVersionList; + +import javax.jms.JMSException; +import javax.security.sasl.SaslClient; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 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 implements ProtocolVersionList +{ + private static final Logger _logger = Logger.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. + /* Find last protocol version in protocol version list. Make sure last protocol version + listed in the build file (build-module.xml) is the latest version which will be used + here. */ + int i = pv.length - 1; + _minaProtocolSession.write(new ProtocolInitiation(pv[i][PROTOCOL_MAJOR], pv[i][PROTOCOL_MINOR])); + } + + 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 getVirtualHost() + { + return getAMQConnection().getVirtualHost(); + } + + 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(message.channelId, message); + } + + public void messageContentHeaderReceived(int channelId, ContentHeaderBody contentHeader) + throws AMQException + { + UnprocessedMessage msg = (UnprocessedMessage) _channelId2UnprocessedMsgMap.get(channelId); + if (msg == null) + { + throw new AMQException("Error: received content header without having received a BasicDeliver 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(channelId); + if (msg == null) + { + throw new AMQException("Error: received content body without having received a JMSDeliver frame first"); + } + if (msg.contentHeader == null) + { + _channelId2UnprocessedMsgMap.remove(channelId); + throw new AMQException("Error: received content body without having received a ContentHeader frame first"); + } + try + { + msg.receiveBody(contentBody); + } + catch (UnexpectedBodyReceivedException e) + { + _channelId2UnprocessedMsgMap.remove(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(channelId); + session.messageReceived(msg); + _channelId2UnprocessedMsgMap.remove(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(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(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(channelId, session); + } + + /** + * 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 = 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++; + } + //todo remove '/' and ':' from local Address as this doesn't conform to spec. + 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)); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java b/java/client/src/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java new file mode 100644 index 0000000000..a7e3ebdd14 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java @@ -0,0 +1,133 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.qpid.AMQException; +import org.apache.qpid.framing.AMQMethodBody; + +public abstract class BlockingMethodFrameListener implements AMQMethodListener +{ + private volatile boolean _ready = false; + + public abstract boolean processMethod(int channelId, AMQMethodBody frame) throws AMQException; + + private final Object _lock = new Object(); + + /** + * This is set if there is an exception thrown from processCommandFrame and the + * exception is rethrown to the caller of blockForFrame() + */ + private volatile Exception _error; + + protected int _channelId; + + protected AMQMethodEvent _doneEvt = null; + + public BlockingMethodFrameListener(int channelId) + { + _channelId = channelId; + } + + /** + * This method is called by the MINA dispatching thread. Note that it could + * be called before blockForFrame() has been called. + * @param evt the frame event + * @return true if the listener has dealt with this frame + * @throws AMQException + */ + public boolean methodReceived(AMQMethodEvent evt) throws AMQException + { + AMQMethodBody method = evt.getMethod(); + + try + { + boolean ready = (evt.getChannelId() == _channelId) && processMethod(evt.getChannelId(), method); + if (ready) + { + // we only update the flag from inside the synchronized block + // so that the blockForFrame method cannot "miss" an update - it + // will only ever read the flag from within the synchronized block + synchronized (_lock) + { + _doneEvt = evt; + _ready = ready; + _lock.notify(); + } + } + return ready; + } + catch (AMQException e) + { + error(e); + // we rethrow the error here, and the code in the frame dispatcher will go round + // each listener informing them that an exception has been thrown + throw e; + } + } + + /** + * This method is called by the thread that wants to wait for a frame. + */ + public AMQMethodEvent blockForFrame() throws AMQException + { + synchronized (_lock) + { + while (!_ready) + { + try + { + _lock.wait(); + } + catch (InterruptedException e) + { + // IGNORE + } + } + } + if (_error != null) + { + if (_error instanceof AMQException) + { + throw (AMQException)_error; + } + else + { + throw new AMQException("Woken up due to exception", _error); // FIXME: This will wrap FailoverException and prevent it being caught. + } + } + + return _doneEvt; + } + + /** + * This is a callback, called by the MINA dispatcher thread only. It is also called from within this + * class to avoid code repetition but again is only called by the MINA dispatcher thread. + * @param e + */ + public void error(Exception e) + { + // set the error so that the thread that is blocking (against blockForFrame()) + // can pick up the exception and rethrow to the caller + _error = e; + synchronized (_lock) + { + _ready = true; + _lock.notify(); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/HeartbeatConfig.java b/java/client/src/org/apache/qpid/client/protocol/HeartbeatConfig.java new file mode 100644 index 0000000000..d6240b919a --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/HeartbeatConfig.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.log4j.Logger; + +class HeartbeatConfig +{ + private static final Logger _logger = Logger.getLogger(HeartbeatConfig.class); + static final HeartbeatConfig CONFIG = new HeartbeatConfig(); + + /** + * The factor used to get the timeout from the delay between heartbeats. + */ + private float timeoutFactor = 2; + + HeartbeatConfig() + { + String property = System.getProperty("amqj.heartbeat.timeoutFactor"); + if(property != null) + { + try + { + timeoutFactor = Float.parseFloat(property); + } + catch(NumberFormatException e) + { + _logger.warn("Invalid timeout factor (amqj.heartbeat.timeoutFactor): " + property); + } + } + } + + float getTimeoutFactor() + { + return timeoutFactor; + } + + int getTimeout(int writeDelay) + { + return (int) (timeoutFactor * writeDelay); + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java b/java/client/src/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java new file mode 100644 index 0000000000..07a9213402 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java @@ -0,0 +1,118 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +class HeartbeatDiagnostics +{ + private static final Diagnostics _impl = init(); + + private static Diagnostics init() + { + return Boolean.getBoolean("amqj.heartbeat.diagnostics") ? new On() : new Off(); + } + + static void sent() + { + _impl.sent(); + } + + static void timeout() + { + _impl.timeout(); + } + + static void received(boolean heartbeat) + { + _impl.received(heartbeat); + } + + static void init(int delay, int timeout) + { + _impl.init(delay, timeout); + } + + private static interface Diagnostics + { + void sent(); + void timeout(); + void received(boolean heartbeat); + void init(int delay, int timeout); + } + + private static class On implements Diagnostics + { + private final String[] messages = new String[50]; + private int i; + + private void save(String msg) + { + messages[i++] = msg; + if(i >= messages.length){ + i = 0;//i.e. a circular buffer + } + } + + public void sent() + { + save(System.currentTimeMillis() + ": sent heartbeat"); + } + + public void timeout() + { + for(int i = 0; i < messages.length; i++) + { + if(messages[i] != null) + { + System.out.println(messages[i]); + } + } + System.out.println(System.currentTimeMillis() + ": timed out"); + } + + public void received(boolean heartbeat) + { + save(System.currentTimeMillis() + ": received " + (heartbeat ? "heartbeat" : "data")); + } + + public void init(int delay, int timeout) + { + System.out.println(System.currentTimeMillis() + ": initialised delay=" + delay + ", timeout=" + timeout); + } + } + + private static class Off implements Diagnostics + { + public void sent() + { + + } + public void timeout() + { + + } + public void received(boolean heartbeat) + { + + } + + public void init(int delay, int timeout) + { + + } + } +} diff --git a/java/client/src/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java b/java/client/src/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java new file mode 100644 index 0000000000..88cbd32764 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java @@ -0,0 +1,110 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.protocol; + +import org.apache.mina.common.IoFilterAdapter; +import org.apache.mina.common.IoSession; +import org.apache.log4j.Logger; + +/** + * A MINA filter that monitors the numbers of messages pending to be sent by MINA. It outputs a message + * when a threshold has been exceeded, and has a frequency configuration so that messages are not output + * too often. + * + */ +public class ProtocolBufferMonitorFilter extends IoFilterAdapter +{ + private static final Logger _logger = Logger.getLogger(ProtocolBufferMonitorFilter.class); + + public static long DEFAULT_FREQUENCY = 5000; + + public static int DEFAULT_THRESHOLD = 3000; + + private int _bufferedMessages = 0; + + private int _threshold; + + private long _lastMessageOutputTime; + + private long _outputFrequencyInMillis; + + public ProtocolBufferMonitorFilter() + { + _threshold = DEFAULT_THRESHOLD; + _outputFrequencyInMillis = DEFAULT_FREQUENCY; + } + + public ProtocolBufferMonitorFilter(int threshold, long frequency) + { + _threshold = threshold; + _outputFrequencyInMillis = frequency; + } + + public void messageReceived( NextFilter nextFilter, IoSession session, Object message ) throws Exception + { + _bufferedMessages++; + if (_bufferedMessages > _threshold) + { + long now = System.currentTimeMillis(); + if ((now - _lastMessageOutputTime) > _outputFrequencyInMillis) + { + _logger.warn("Protocol message buffer exceeded threshold of " + _threshold + ". Current backlog: " + + _bufferedMessages); + _lastMessageOutputTime = now; + } + } + + nextFilter.messageReceived(session, message); + } + + public void messageSent( NextFilter nextFilter, IoSession session, Object message ) throws Exception + { + _bufferedMessages--; + nextFilter.messageSent(session, message); + } + + public int getBufferedMessages() + { + return _bufferedMessages; + } + + public int getThreshold() + { + return _threshold; + } + + public void setThreshold(int threshold) + { + _threshold = threshold; + } + + public long getOutputFrequencyInMillis() + { + return _outputFrequencyInMillis; + } + + public void setOutputFrequencyInMillis(long outputFrequencyInMillis) + { + _outputFrequencyInMillis = outputFrequencyInMillis; + } + + public long getLastMessageOutputTime() + { + return _lastMessageOutputTime; + } +} diff --git a/java/client/src/org/apache/qpid/client/security/AMQCallbackHandler.java b/java/client/src/org/apache/qpid/client/security/AMQCallbackHandler.java new file mode 100644 index 0000000000..43e1e6ab70 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/AMQCallbackHandler.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security; + +import org.apache.qpid.client.protocol.AMQProtocolSession; + +import javax.security.auth.callback.CallbackHandler; + +public interface AMQCallbackHandler extends CallbackHandler +{ + void initialise(AMQProtocolSession protocolSession); +} diff --git a/java/client/src/org/apache/qpid/client/security/CallbackHandlerRegistry.java b/java/client/src/org/apache/qpid/client/security/CallbackHandlerRegistry.java new file mode 100644 index 0000000000..8bfa0c32c4 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/CallbackHandlerRegistry.java @@ -0,0 +1,154 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security; + +import org.apache.log4j.Logger; + +import java.io.*; +import java.util.*; + +public class CallbackHandlerRegistry +{ + private static final String FILE_PROPERTY = "amq.callbackhandler.properties"; + + private static final Logger _logger = Logger.getLogger(CallbackHandlerRegistry.class); + + private static CallbackHandlerRegistry _instance = new CallbackHandlerRegistry(); + + private Map _mechanismToHandlerClassMap = new HashMap(); + + private String _mechanisms; + + public static CallbackHandlerRegistry getInstance() + { + return _instance; + } + + public Class getCallbackHandlerClass(String mechanism) + { + return (Class) _mechanismToHandlerClassMap.get(mechanism); + } + + public String getMechanisms() + { + return _mechanisms; + } + + private CallbackHandlerRegistry() + { + // first we register any Sasl client factories + DynamicSaslRegistrar.registerSaslProviders(); + + InputStream is = openPropertiesInputStream(); + try + { + Properties props = new Properties(); + props.load(is); + parseProperties(props); + _logger.info("Available SASL mechanisms: " + _mechanisms); + } + catch (IOException e) + { + _logger.error("Error reading properties: " + e, e); + } + finally + { + if (is != null) + { + try + { + is.close(); + + } + catch (IOException e) + { + _logger.error("Unable to close properties stream: " + e, e); + } + } + } + } + + private InputStream openPropertiesInputStream() + { + String filename = System.getProperty(FILE_PROPERTY); + boolean useDefault = true; + InputStream is = null; + if (filename != null) + { + try + { + is = new BufferedInputStream(new FileInputStream(new File(filename))); + useDefault = false; + } + catch (FileNotFoundException e) + { + _logger.error("Unable to read from file " + filename + ": " + e, e); + } + } + + if (useDefault) + { + is = CallbackHandlerRegistry.class.getResourceAsStream("CallbackHandlerRegistry.properties"); + } + + return is; + } + + private void parseProperties(Properties props) + { + Enumeration e = props.propertyNames(); + while (e.hasMoreElements()) + { + String propertyName = (String) e.nextElement(); + int period = propertyName.indexOf("."); + if (period < 0) + { + _logger.warn("Unable to parse property " + propertyName + " when configuring SASL providers"); + continue; + } + String mechanism = propertyName.substring(period + 1); + String className = props.getProperty(propertyName); + Class clazz = null; + try + { + clazz = Class.forName(className); + if (!AMQCallbackHandler.class.isAssignableFrom(clazz)) + { + _logger.warn("SASL provider " + clazz + " does not implement " + AMQCallbackHandler.class + + ". Skipping"); + continue; + } + _mechanismToHandlerClassMap.put(mechanism, clazz); + if (_mechanisms == null) + { + _mechanisms = mechanism; + } + else + { + // one time cost + _mechanisms = _mechanisms + " " + mechanism; + } + } + catch (ClassNotFoundException ex) + { + _logger.warn("Unable to load class " + className + ". Skipping that SASL provider"); + continue; + } + } + } +} diff --git a/java/client/src/org/apache/qpid/client/security/CallbackHandlerRegistry.properties b/java/client/src/org/apache/qpid/client/security/CallbackHandlerRegistry.properties new file mode 100644 index 0000000000..357f9a1199 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/CallbackHandlerRegistry.properties @@ -0,0 +1,2 @@ +CallbackHandler.CRAM-MD5=org.apache.qpid.client.security.UsernamePasswordCallbackHandler +CallbackHandler.PLAIN=org.apache.qpid.client.security.UsernamePasswordCallbackHandler
\ No newline at end of file diff --git a/java/client/src/org/apache/qpid/client/security/DynamicSaslRegistrar.java b/java/client/src/org/apache/qpid/client/security/DynamicSaslRegistrar.java new file mode 100644 index 0000000000..3d81ad505a --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/DynamicSaslRegistrar.java @@ -0,0 +1,125 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security; + +import org.apache.log4j.Logger; + +import javax.security.sasl.SaslClientFactory; +import java.io.*; +import java.util.Properties; +import java.util.Enumeration; +import java.util.Map; +import java.util.TreeMap; +import java.security.Security; + +public class DynamicSaslRegistrar +{ + private static final String FILE_PROPERTY = "amq.dynamicsaslregistrar.properties"; + + private static final Logger _logger = Logger.getLogger(DynamicSaslRegistrar.class); + + public static void registerSaslProviders() + { + InputStream is = openPropertiesInputStream(); + try + { + Properties props = new Properties(); + props.load(is); + Map<String, Class<? extends SaslClientFactory>> factories = parseProperties(props); + if (factories.size() > 0) + { + Security.addProvider(new JCAProvider(factories)); + _logger.debug("Dynamic SASL provider added as a security provider"); + } + } + catch (IOException e) + { + _logger.error("Error reading properties: " + e, e); + } + finally + { + if (is != null) + { + try + { + is.close(); + + } + catch (IOException e) + { + _logger.error("Unable to close properties stream: " + e, e); + } + } + } + } + + private static InputStream openPropertiesInputStream() + { + String filename = System.getProperty(FILE_PROPERTY); + boolean useDefault = true; + InputStream is = null; + if (filename != null) + { + try + { + is = new BufferedInputStream(new FileInputStream(new File(filename))); + useDefault = false; + } + catch (FileNotFoundException e) + { + _logger.error("Unable to read from file " + filename + ": " + e, e); + } + } + + if (useDefault) + { + is = CallbackHandlerRegistry.class.getResourceAsStream("DynamicSaslRegistrar.properties"); + } + + return is; + } + + private static Map<String, Class<? extends SaslClientFactory>> parseProperties(Properties props) + { + Enumeration e = props.propertyNames(); + TreeMap<String, Class<? extends SaslClientFactory>> factoriesToRegister = + new TreeMap<String, Class<? extends SaslClientFactory>>(); + while (e.hasMoreElements()) + { + String mechanism = (String) e.nextElement(); + String className = props.getProperty(mechanism); + try + { + Class<?> clazz = Class.forName(className); + if (!(SaslClientFactory.class.isAssignableFrom(clazz))) + { + _logger.error("Class " + clazz + " does not implement " + SaslClientFactory.class + " - skipping"); + continue; + } + factoriesToRegister.put(mechanism, (Class<? extends SaslClientFactory>) clazz); + } + catch (Exception ex) + { + _logger.error("Error instantiating SaslClientFactory calss " + className + " - skipping"); + } + } + return factoriesToRegister; + } + + +} diff --git a/java/client/src/org/apache/qpid/client/security/DynamicSaslRegistrar.properties b/java/client/src/org/apache/qpid/client/security/DynamicSaslRegistrar.properties new file mode 100644 index 0000000000..b853d86d65 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/DynamicSaslRegistrar.properties @@ -0,0 +1 @@ +AMQPLAIN=org.apache.qpid.client.security.amqplain.AmqPlainSaslClientFactory
\ No newline at end of file diff --git a/java/client/src/org/apache/qpid/client/security/JCAProvider.java b/java/client/src/org/apache/qpid/client/security/JCAProvider.java new file mode 100644 index 0000000000..4a14063793 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/JCAProvider.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security; + +import javax.security.sasl.SaslClientFactory; +import java.security.Provider; +import java.security.Security; +import java.util.Map; + +public class JCAProvider extends Provider +{ + public JCAProvider(Map<String, Class<? extends SaslClientFactory>> providerMap) + { + super("AMQSASLProvider", 1.0, "A JCA provider that registers all " + + "AMQ SASL providers that want to be registered"); + register(providerMap); + Security.addProvider(this); + } + + private void register(Map<String, Class<? extends SaslClientFactory>> providerMap) + { + for (Map.Entry<String, Class<? extends SaslClientFactory>> me : + providerMap.entrySet()) + { + put("SaslClientFactory." + me.getKey(), me.getValue().getName()); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java b/java/client/src/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java new file mode 100644 index 0000000000..201cc04726 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java @@ -0,0 +1,53 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security; + +import org.apache.qpid.client.protocol.AMQProtocolSession; + +import javax.security.auth.callback.*; +import java.io.IOException; + +public class UsernamePasswordCallbackHandler implements AMQCallbackHandler +{ + private AMQProtocolSession _protocolSession; + + public void initialise(AMQProtocolSession protocolSession) + { + _protocolSession = protocolSession; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException + { + for (int i = 0; i < callbacks.length; i++) + { + Callback cb = callbacks[i]; + if (cb instanceof NameCallback) + { + ((NameCallback)cb).setName(_protocolSession.getUsername()); + } + else if (cb instanceof PasswordCallback) + { + ((PasswordCallback)cb).setPassword(_protocolSession.getPassword().toCharArray()); + } + else + { + throw new UnsupportedCallbackException(cb); + } + } + } +} diff --git a/java/client/src/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java b/java/client/src/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java new file mode 100644 index 0000000000..d564e82ec3 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java @@ -0,0 +1,101 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security.amqplain; + +import org.apache.qpid.framing.FieldTable; + +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.Callback; + +/** + * Implements the "AMQPlain" authentication protocol that uses FieldTables to send username and pwd. + * + */ +public class AmqPlainSaslClient implements SaslClient +{ + /** + * The name of this mechanism + */ + public static final String MECHANISM = "AMQPLAIN"; + + private CallbackHandler _cbh; + + public AmqPlainSaslClient(CallbackHandler cbh) + { + _cbh = cbh; + } + + public String getMechanismName() + { + return "AMQPLAIN"; + } + + public boolean hasInitialResponse() + { + return true; + } + + public byte[] evaluateChallenge(byte[] challenge) throws SaslException + { + // we do not care about the prompt or the default name + NameCallback nameCallback = new NameCallback("prompt", "defaultName"); + PasswordCallback pwdCallback = new PasswordCallback("prompt", false); + Callback[] callbacks = new Callback[]{nameCallback, pwdCallback}; + try + { + _cbh.handle(callbacks); + } + catch (Exception e) + { + throw new SaslException("Error handling SASL callbacks: " + e, e); + } + FieldTable table = new FieldTable(); + table.put("LOGIN", nameCallback.getName()); + table.put("PASSWORD", pwdCallback.getPassword()); + return table.getDataAsBytes(); + } + + public boolean isComplete() + { + return true; + } + + public byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException + { + throw new SaslException("Not supported"); + } + + public byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException + { + throw new SaslException("Not supported"); + } + + public Object getNegotiatedProperty(String propName) + { + return null; + } + + public void dispose() throws SaslException + { + _cbh = null; + } +} diff --git a/java/client/src/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java b/java/client/src/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java new file mode 100644 index 0000000000..b4cc34dd03 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.security.amqplain; + +import javax.security.sasl.SaslClientFactory; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import javax.security.sasl.Sasl; +import javax.security.auth.callback.CallbackHandler; +import java.util.Map; + +public class AmqPlainSaslClientFactory implements SaslClientFactory +{ + public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol, String serverName, Map props, CallbackHandler cbh) throws SaslException + { + for (int i = 0; i < mechanisms.length; i++) + { + if (mechanisms[i].equals(AmqPlainSaslClient.MECHANISM)) + { + if (cbh == null) + { + throw new SaslException("CallbackHandler must not be null"); + } + return new AmqPlainSaslClient(cbh); + } + } + return null; + } + + public String[] getMechanismNames(Map props) + { + if (props.containsKey(Sasl.POLICY_NOPLAINTEXT) || + props.containsKey(Sasl.POLICY_NODICTIONARY) || + props.containsKey(Sasl.POLICY_NOACTIVE)) + { + // returned array must be non null according to interface documentation + return new String[0]; + } + else + { + return new String[]{AmqPlainSaslClient.MECHANISM}; + } + } +} diff --git a/java/client/src/org/apache/qpid/client/state/AMQState.java b/java/client/src/org/apache/qpid/client/state/AMQState.java new file mode 100644 index 0000000000..02a391ee71 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/AMQState.java @@ -0,0 +1,53 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +/** + * States used in the AMQ protocol. Used by the finite state machine to determine + * valid responses. + */ +public class AMQState +{ + private final int _id; + + private final String _name; + + private AMQState(int id, String name) + { + _id = id; + _name = name; + } + + public String toString() + { + return "AMQState: id = " + _id + " name: " + _name; + } + + public static final AMQState CONNECTION_NOT_STARTED = new AMQState(1, "CONNECTION_NOT_STARTED"); + + public static final AMQState CONNECTION_NOT_TUNED = new AMQState(2, "CONNECTION_NOT_TUNED"); + + public static final AMQState CONNECTION_NOT_OPENED = new AMQState(3, "CONNECTION_NOT_OPENED"); + + public static final AMQState CONNECTION_OPEN = new AMQState(4, "CONNECTION_OPEN"); + + public static final AMQState CONNECTION_CLOSING = new AMQState(5, "CONNECTION_CLOSING"); + + public static final AMQState CONNECTION_CLOSED = new AMQState(6, "CONNECTION_CLOSED"); + +} diff --git a/java/client/src/org/apache/qpid/client/state/AMQStateChangedEvent.java b/java/client/src/org/apache/qpid/client/state/AMQStateChangedEvent.java new file mode 100644 index 0000000000..91955e37fd --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/AMQStateChangedEvent.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +/** + * An event that is fired when the protocol state has changed. + * + */ +public class AMQStateChangedEvent +{ + private final AMQState _oldState; + + private final AMQState _newState; + + public AMQStateChangedEvent(AMQState oldState, AMQState newState) + { + _oldState = oldState; + _newState = newState; + } + + public AMQState getOldState() + { + return _oldState; + } + + public AMQState getNewState() + { + return _newState; + } +} diff --git a/java/client/src/org/apache/qpid/client/state/AMQStateListener.java b/java/client/src/org/apache/qpid/client/state/AMQStateListener.java new file mode 100644 index 0000000000..7f9698dc92 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/AMQStateListener.java @@ -0,0 +1,23 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +public interface AMQStateListener +{ + void stateChanged(AMQStateChangedEvent evt); +} diff --git a/java/client/src/org/apache/qpid/client/state/AMQStateManager.java b/java/client/src/org/apache/qpid/client/state/AMQStateManager.java new file mode 100644 index 0000000000..cdabcf1df4 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/AMQStateManager.java @@ -0,0 +1,224 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +import org.apache.qpid.AMQException; +import org.apache.qpid.client.handler.*; +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.client.protocol.AMQMethodListener; +import org.apache.qpid.framing.*; +import org.apache.log4j.Logger; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * The state manager is responsible for managing the state of the protocol session. + * <p/> + * For each AMQProtocolHandler there is a separate state manager. + */ +public class AMQStateManager implements AMQMethodListener +{ + private static final Logger _logger = Logger.getLogger(AMQStateManager.class); + + /** + * The current state + */ + private AMQState _currentState; + + /** + * Maps from an AMQState instance to a Map from Class to StateTransitionHandler. + * The class must be a subclass of AMQFrame. + */ + private final Map _state2HandlersMap = new HashMap(); + + private final CopyOnWriteArraySet _stateListeners = new CopyOnWriteArraySet(); + + public AMQStateManager() + { + this(AMQState.CONNECTION_NOT_STARTED, true); + } + + protected AMQStateManager(AMQState state, boolean register) + { + _currentState = state; + if(register) + { + registerListeners(); + } + } + + protected void registerListeners() + { + Map frame2handlerMap = new HashMap(); + + // we need to register a map for the null (i.e. all state) handlers otherwise you get + // a stack overflow in the handler searching code when you present it with a frame for which + // no handlers are registered + // + _state2HandlersMap.put(null, frame2handlerMap); + + frame2handlerMap = new HashMap(); + frame2handlerMap.put(ConnectionStartBody.class, ConnectionStartMethodHandler.getInstance()); + frame2handlerMap.put(ConnectionCloseBody.class, ConnectionCloseMethodHandler.getInstance()); + _state2HandlersMap.put(AMQState.CONNECTION_NOT_STARTED, frame2handlerMap); + + frame2handlerMap = new HashMap(); + frame2handlerMap.put(ConnectionTuneBody.class, ConnectionTuneMethodHandler.getInstance()); + frame2handlerMap.put(ConnectionSecureBody.class, ConnectionSecureMethodHandler.getInstance()); + frame2handlerMap.put(ConnectionCloseBody.class, ConnectionCloseMethodHandler.getInstance()); + _state2HandlersMap.put(AMQState.CONNECTION_NOT_TUNED, frame2handlerMap); + + frame2handlerMap = new HashMap(); + frame2handlerMap.put(ConnectionOpenOkBody.class, ConnectionOpenOkMethodHandler.getInstance()); + frame2handlerMap.put(ConnectionCloseBody.class, ConnectionCloseMethodHandler.getInstance()); + _state2HandlersMap.put(AMQState.CONNECTION_NOT_OPENED, frame2handlerMap); + + // + // ConnectionOpen handlers + // + frame2handlerMap = new HashMap(); + frame2handlerMap.put(ChannelCloseBody.class, ChannelCloseMethodHandler.getInstance()); + frame2handlerMap.put(ChannelCloseOkBody.class, ChannelCloseOkMethodHandler.getInstance()); + frame2handlerMap.put(ConnectionCloseBody.class, ConnectionCloseMethodHandler.getInstance()); + frame2handlerMap.put(BasicDeliverBody.class, BasicDeliverMethodHandler.getInstance()); + frame2handlerMap.put(BasicReturnBody.class, BasicReturnMethodHandler.getInstance()); + frame2handlerMap.put(ChannelFlowOkBody.class, ChannelFlowOkMethodHandler.getInstance()); + _state2HandlersMap.put(AMQState.CONNECTION_OPEN, frame2handlerMap); + } + + public AMQState getCurrentState() + { + return _currentState; + } + + public void changeState(AMQState newState) throws AMQException + { + _logger.debug("State changing to " + newState + " from old state " + _currentState); + final AMQState oldState = _currentState; + _currentState = newState; + + synchronized (_stateListeners) + { + final Iterator it = _stateListeners.iterator(); + while (it.hasNext()) + { + final StateListener l = (StateListener) it.next(); + l.stateChanged(oldState, newState); + } + } + } + + public void error(Exception e) + { + _logger.debug("State manager receive error notification: " + e); + synchronized (_stateListeners) + { + final Iterator it = _stateListeners.iterator(); + while (it.hasNext()) + { + final StateListener l = (StateListener) it.next(); + l.error(e); + } + } + } + + public boolean methodReceived(AMQMethodEvent evt) throws AMQException + { + StateAwareMethodListener handler = findStateTransitionHandler(_currentState, evt.getMethod()); + if (handler != null) + { + handler.methodReceived(this, evt); + return true; + } + return false; + } + + protected StateAwareMethodListener findStateTransitionHandler(AMQState currentState, + AMQMethodBody frame) + throws IllegalStateTransitionException + { + final Class clazz = frame.getClass(); + if (_logger.isDebugEnabled()) + { + _logger.debug("Looking for state transition handler for frame " + clazz); + } + final Map classToHandlerMap = (Map) _state2HandlersMap.get(currentState); + + if (classToHandlerMap == null) + { + // if no specialised per state handler is registered look for a + // handler registered for "all" states + return findStateTransitionHandler(null, frame); + } + final StateAwareMethodListener handler = (StateAwareMethodListener) classToHandlerMap.get(clazz); + if (handler == null) + { + if (currentState == null) + { + _logger.debug("No state transition handler defined for receiving frame " + frame); + return null; + } + else + { + // if no specialised per state handler is registered look for a + // handler registered for "all" states + return findStateTransitionHandler(null, frame); + } + } + else + { + return handler; + } + } + + public void addStateListener(StateListener listener) + { + _logger.debug("Adding state listener"); + _stateListeners.add(listener); + } + + public void removeStateListener(StateListener listener) + { + _stateListeners.remove(listener); + } + + public void attainState(AMQState s) throws AMQException + { + boolean needToWait = false; + StateWaiter sw = null; + synchronized (_stateListeners) + { + if (_currentState != s) + { + _logger.debug("Adding state wait to reach state " + s); + sw = new StateWaiter(s); + addStateListener(sw); + // we use a boolean since we must release the lock before starting to wait + needToWait = true; + } + } + if (needToWait) + { + sw.waituntilStateHasChanged(); + } + // at this point the state will have changed. + } +} diff --git a/java/client/src/org/apache/qpid/client/state/IllegalStateTransitionException.java b/java/client/src/org/apache/qpid/client/state/IllegalStateTransitionException.java new file mode 100644 index 0000000000..58bb38352b --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/IllegalStateTransitionException.java @@ -0,0 +1,45 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +import org.apache.qpid.AMQException; + +public class IllegalStateTransitionException extends AMQException +{ + private AMQState _originalState; + + private Class _frame; + + public IllegalStateTransitionException(AMQState originalState, Class frame) + { + super("No valid state transition defined for receiving frame " + frame + + " from state " + originalState); + _originalState = originalState; + _frame = frame; + } + + public AMQState getOriginalState() + { + return _originalState; + } + + public Class getFrameClass() + { + return _frame; + } +} diff --git a/java/client/src/org/apache/qpid/client/state/StateAwareMethodListener.java b/java/client/src/org/apache/qpid/client/state/StateAwareMethodListener.java new file mode 100644 index 0000000000..6fed4f2df6 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/StateAwareMethodListener.java @@ -0,0 +1,31 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +import org.apache.qpid.client.protocol.AMQMethodEvent; +import org.apache.qpid.AMQException; + +/** + * A frame listener that is informed of the protocl state when invoked and has + * the opportunity to update state. + * + */ +public interface StateAwareMethodListener +{ + void methodReceived(AMQStateManager stateManager, AMQMethodEvent evt) throws AMQException; +} diff --git a/java/client/src/org/apache/qpid/client/state/StateListener.java b/java/client/src/org/apache/qpid/client/state/StateListener.java new file mode 100644 index 0000000000..0a5bee5106 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/StateListener.java @@ -0,0 +1,27 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +import org.apache.qpid.AMQException; + +public interface StateListener +{ + void stateChanged(AMQState oldState, AMQState newState) throws AMQException; + + void error(Throwable t); +} diff --git a/java/client/src/org/apache/qpid/client/state/StateWaiter.java b/java/client/src/org/apache/qpid/client/state/StateWaiter.java new file mode 100644 index 0000000000..53ce452b78 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/StateWaiter.java @@ -0,0 +1,114 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; + +/** + * Waits for a particular state to be reached. + * + */ +public class StateWaiter implements StateListener +{ + private static final Logger _logger = Logger.getLogger(StateWaiter.class); + + private final AMQState _state; + + private volatile boolean _newStateAchieved; + + private volatile Throwable _throwable; + + private final Object _monitor = new Object(); + + public StateWaiter(AMQState state) + { + _state = state; + } + + public void waituntilStateHasChanged() throws AMQException + { + synchronized (_monitor) + { + // + // The guard is required in case we are woken up by a spurious + // notify(). + // + while (!_newStateAchieved && _throwable == null) + { + try + { + _logger.debug("State " + _state + " not achieved so waiting..."); + _monitor.wait(); + } + catch (InterruptedException e) + { + _logger.debug("Interrupted exception caught while waiting: " + e, e); + } + } + } + + if (_throwable != null) + { + _logger.debug("Throwable reached state waiter: " + _throwable); + if (_throwable instanceof AMQException) + { + throw (AMQException) _throwable; + } + else + { + throw new AMQException("Error: " + _throwable, _throwable); // FIXME: this will wrap FailoverException in throwable which will prevent it being caught. + } + } + } + + public void stateChanged(AMQState oldState, AMQState newState) + { + synchronized (_monitor) + { + if (_logger.isDebugEnabled()) + { + _logger.debug("stateChanged called"); + } + if (_state == newState) + { + _newStateAchieved = true; + + if (_logger.isDebugEnabled()) + { + _logger.debug("New state reached so notifying monitor"); + } + _monitor.notifyAll(); + } + } + } + + public void error(Throwable t) + { + synchronized (_monitor) + { + if (_logger.isDebugEnabled()) + { + _logger.debug("exceptionThrown called"); + } + + _throwable = t; + _monitor.notifyAll(); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java b/java/client/src/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java new file mode 100644 index 0000000000..b2d7d777d8 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java @@ -0,0 +1,38 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.state.listener; + +import org.apache.qpid.client.protocol.BlockingMethodFrameListener; +import org.apache.qpid.framing.AMQMethodBody; +import org.apache.qpid.AMQException; + +public class SpecificMethodFrameListener extends BlockingMethodFrameListener +{ + private final Class _expectedClass; + + public SpecificMethodFrameListener(int channelId, Class expectedClass) + { + super(channelId); + _expectedClass = expectedClass; + } + + public boolean processMethod(int channelId, AMQMethodBody frame) throws AMQException + { + return _expectedClass.isInstance(frame); + } +} diff --git a/java/client/src/org/apache/qpid/client/transport/ITransportConnection.java b/java/client/src/org/apache/qpid/client/transport/ITransportConnection.java new file mode 100644 index 0000000000..49af2d1bdb --- /dev/null +++ b/java/client/src/org/apache/qpid/client/transport/ITransportConnection.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.transport; + +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.jms.BrokerDetails; + +import java.io.IOException; + +public interface ITransportConnection +{ + void connect(AMQProtocolHandler protocolHandler, BrokerDetails brokerDetail) + throws IOException; +} diff --git a/java/client/src/org/apache/qpid/client/transport/SocketTransportConnection.java b/java/client/src/org/apache/qpid/client/transport/SocketTransportConnection.java new file mode 100644 index 0000000000..0ec5ba2473 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/transport/SocketTransportConnection.java @@ -0,0 +1,96 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.transport; + +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.pool.ReadWriteThreadModel; +import org.apache.qpid.jms.BrokerDetails; +import org.apache.log4j.Logger; +import org.apache.mina.common.ByteBuffer; +import org.apache.mina.common.ConnectFuture; +import org.apache.mina.common.IoConnector; +import org.apache.mina.common.SimpleByteBufferAllocator; +import org.apache.mina.transport.socket.nio.SocketConnectorConfig; +import org.apache.mina.transport.socket.nio.SocketSessionConfig; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public class SocketTransportConnection implements ITransportConnection +{ + private static final Logger _logger = Logger.getLogger(SocketTransportConnection.class); + private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; + + private SocketConnectorFactory _socketConnectorFactory; + + static interface SocketConnectorFactory { + IoConnector newSocketConnector(); + } + + public SocketTransportConnection(SocketConnectorFactory socketConnectorFactory) + { + _socketConnectorFactory = socketConnectorFactory; + } + + public void connect(AMQProtocolHandler protocolHandler, BrokerDetails brokerDetail) + throws IOException + { + ByteBuffer.setUseDirectBuffers(Boolean.getBoolean("amqj.enableDirectBuffers")); + + // the MINA default is currently to use the pooled allocator although this may change in future + // once more testing of the performance of the simple allocator has been done + if (!Boolean.getBoolean("amqj.enablePooledAllocator")) + { + ByteBuffer.setAllocator(new SimpleByteBufferAllocator()); + } + + final IoConnector ioConnector = _socketConnectorFactory.newSocketConnector(); + SocketConnectorConfig cfg = (SocketConnectorConfig) ioConnector.getDefaultConfig(); + + // if we do not use our own thread model we get the MINA default which is to use + // its own leader-follower model + boolean readWriteThreading = Boolean.getBoolean("amqj.shared_read_write_pool"); + if (readWriteThreading) + { + cfg.setThreadModel(new ReadWriteThreadModel()); + } + + SocketSessionConfig scfg = (SocketSessionConfig) cfg.getSessionConfig(); + scfg.setTcpNoDelay("true".equalsIgnoreCase(System.getProperty("amqj.tcpNoDelay", "true"))); + scfg.setSendBufferSize(Integer.getInteger("amqj.sendBufferSize", DEFAULT_BUFFER_SIZE)); + _logger.info("send-buffer-size = " + scfg.getSendBufferSize()); + scfg.setReceiveBufferSize(Integer.getInteger("amqj.receiveBufferSize", DEFAULT_BUFFER_SIZE)); + _logger.info("recv-buffer-size = " + scfg.getReceiveBufferSize()); + final InetSocketAddress address = new InetSocketAddress(brokerDetail.getHost(), brokerDetail.getPort()); + protocolHandler.setUseSSL(brokerDetail.useSSL()); + _logger.info("Attempting connection to " + address); + ConnectFuture future = ioConnector.connect(address, protocolHandler); + + // wait for connection to complete + if (future.join(brokerDetail.getTimeout())) + { + // we call getSession which throws an IOException if there has been an error connecting + future.getSession(); + } + else + { + throw new IOException("Timeout waiting for connection."); + } + } +} diff --git a/java/client/src/org/apache/qpid/client/transport/TransportConnection.java b/java/client/src/org/apache/qpid/client/transport/TransportConnection.java new file mode 100644 index 0000000000..a898e182f7 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/transport/TransportConnection.java @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.transport; + +import org.apache.mina.common.IoConnector; +import org.apache.mina.transport.socket.nio.SocketConnector; + +/** + * The TransportConnection is a helper class responsible for connecting to an AMQ server. It sets up + * the underlying connector, which currently always uses TCP/IP sockets. It creates the + * "protocol handler" which deals with MINA protocol events. + * + * Could be extended in future to support different transport types by turning this into concrete class/interface + * combo. + */ +public class TransportConnection +{ + private static ITransportConnection _instance; + + static + { + if (Boolean.getBoolean("amqj.useBlockingIo")) + { + _instance = new SocketTransportConnection(new SocketTransportConnection.SocketConnectorFactory() { + public IoConnector newSocketConnector() { + return new org.apache.qpid.bio.SocketConnector(); // blocking connector + } + }); + } + else + { + _instance = new SocketTransportConnection(new SocketTransportConnection.SocketConnectorFactory() { + public IoConnector newSocketConnector() { + SocketConnector result = new SocketConnector(); // non-blocking connector + + // Don't have the connector's worker thread wait around for other connections (we only use + // one SocketConnector per connection at the moment anyway). This allows short-running + // clients (like unit tests) to complete quickly. + result.setWorkerTimeout(0L); + + return result; + } + }); + } + } + + public static void setInstance(ITransportConnection transport) + { + _instance = transport; + } + + public static ITransportConnection getInstance() + { + return _instance; + } +} diff --git a/java/client/src/org/apache/qpid/client/util/FlowControllingBlockingQueue.java b/java/client/src/org/apache/qpid/client/util/FlowControllingBlockingQueue.java new file mode 100644 index 0000000000..ad2ca7b731 --- /dev/null +++ b/java/client/src/org/apache/qpid/client/util/FlowControllingBlockingQueue.java @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.client.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A blocking queue that emits events above a user specified threshold allowing + * the caller to take action (e.g. flow control) to try to prevent the queue + * growing (much) further. The underlying queue itself is not bounded therefore + * the caller is not obliged to react to the events. + * <p/> + * This implementation is <b>only</b> safe where we have a single thread adding + * items and a single (different) thread removing items. + * + */ +public class FlowControllingBlockingQueue +{ + /** + * This queue is bounded and is used to store messages before being dispatched to the consumer + */ + private final BlockingQueue _queue = new LinkedBlockingQueue(); + + private final int _flowControlThreshold; + + private final ThresholdListener _listener; + + /** + * We require a separate count so we can track whether we have reached the + * threshold + */ + private int _count; + + public interface ThresholdListener + { + void aboveThreshold(int currentValue); + + void underThreshold(int currentValue); + } + + public FlowControllingBlockingQueue(int threshold, ThresholdListener listener) + { + _flowControlThreshold = threshold; + _listener = listener; + } + + public Object take() throws InterruptedException + { + Object o = _queue.take(); + synchronized (_listener) + { + if (--_count == (_flowControlThreshold - 1)) + { + _listener.underThreshold(_count); + } + } + return o; + } + + public void add(Object o) + { + _queue.add(o); + synchronized (_listener) + { + if (++_count == _flowControlThreshold) + { + _listener.aboveThreshold(_count); + } + } + } +} diff --git a/java/client/src/org/apache/qpid/jms/BrokerDetails.java b/java/client/src/org/apache/qpid/jms/BrokerDetails.java new file mode 100644 index 0000000000..fc8af2091e --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/BrokerDetails.java @@ -0,0 +1,59 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +public interface BrokerDetails +{ + + /* + * Known URL Options + * @see ConnectionURL + */ + public static final String OPTIONS_RETRY = "retries"; + public static final String OPTIONS_SSL = ConnectionURL.OPTIONS_SSL; + public static final String OPTIONS_CONNECT_TIMEOUT = "connecttimeout"; + public static final int DEFAULT_PORT = 5672; + public static final String DEFAULT_TRANSPORT = "tcp"; + + public static final String URL_FORMAT_EXAMPLE = + "<transport>://<hostname>[:<port Default=\""+DEFAULT_PORT+"\">][?<option>='<value>'[,<option>='<value>']]"; + + public static final long DEFAULT_CONNECT_TIMEOUT = 30000L; + + String getHost(); + void setHost(String host); + + int getPort(); + void setPort(int port); + + String getTransport(); + void setTransport(String transport); + + boolean useSSL(); + void useSSL(boolean ssl); + + String getOption(String key); + void setOption(String key,String value); + + long getTimeout(); + void setTimeout(long timeout); + + String toString(); + + boolean equals(Object o); +} diff --git a/java/client/src/org/apache/qpid/jms/ChannelLimitReachedException.java b/java/client/src/org/apache/qpid/jms/ChannelLimitReachedException.java new file mode 100644 index 0000000000..d18831f5ff --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/ChannelLimitReachedException.java @@ -0,0 +1,43 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +import javax.jms.ResourceAllocationException; + +/** + * Indicates that the maximum number of sessions per connection limit has been reached. + */ +public class ChannelLimitReachedException extends ResourceAllocationException +{ + private static final String ERROR_CODE = "1"; + + private long _limit; + + public ChannelLimitReachedException(long limit) + { + super("Unable to create session since maximum number of sessions per connection is " + + limit + ". Either close one or more sessions or increase the " + + "maximum number of sessions per connection (or contact your AMQP administrator.", ERROR_CODE); + _limit = limit; + } + + public long getLimit() + { + return _limit; + } +} diff --git a/java/client/src/org/apache/qpid/jms/Connection.java b/java/client/src/org/apache/qpid/jms/Connection.java new file mode 100644 index 0000000000..88cfdfc65f --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/Connection.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +import javax.jms.*; + + +public interface Connection extends javax.jms.Connection +{ + /** + * @return the maximum number of sessions supported by this Connection + */ + long getMaximumChannelCount(); + + void setConnectionListener(ConnectionListener listener); + + /** + * Get the connection listener that has been registered with this connection, if any + * @return the listener or null if none has been set + */ + ConnectionListener getConnectionListener(); + + /** + * Create a session specifying the prefetch limit of messages. + * @param transacted + * @param acknowledgeMode + * @param prefetch the maximum number of messages to buffer in the client. This + * applies as a total across all consumers + * @return + * @throws JMSException + */ + org.apache.qpid.jms.Session createSession(boolean transacted, int acknowledgeMode, + int prefetch) throws JMSException; +} diff --git a/java/client/src/org/apache/qpid/jms/ConnectionListener.java b/java/client/src/org/apache/qpid/jms/ConnectionListener.java new file mode 100644 index 0000000000..2ffcf0c8fc --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/ConnectionListener.java @@ -0,0 +1,55 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +public interface ConnectionListener +{ + /** + * Called when bytes have been transmitted to the server + * @param count the number of bytes sent in total since the connection was opened + */ + void bytesSent(long count); + + /** + * Called when some bytes have been received on a connection + * @param count the number of bytes received in total since the connection was opened + */ + void bytesReceived(long count); + + /** + * Called after the infrastructure has detected that failover is required but before attempting failover. + * @param redirect true if the broker requested redirect. false if failover is occurring due to a connection error. + * @return true to continue failing over, false to veto failover and raise a connection exception + */ + boolean preFailover(boolean redirect); + + /** + * Called after connection has been made to another broker after failover has been started but before + * any resubscription has been done. + * @return true to continue with resubscription, false to prevent automatic resubscription. This is useful in + * cases where the application wants to handle resubscription. Note that in the latter case all sessions, producers + * and consumers are invalidated. + */ + boolean preResubscribe(); + + /** + * Called once failover has completed successfully. This is called irrespective of whether the client has + * vetoed automatic resubscription. + */ + void failoverComplete(); +} diff --git a/java/client/src/org/apache/qpid/jms/ConnectionURL.java b/java/client/src/org/apache/qpid/jms/ConnectionURL.java new file mode 100644 index 0000000000..555bbd2f46 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/ConnectionURL.java @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +import java.util.List; + +/** + Connection URL format + amqp://[user:pass@][clientid]/virtualhost?brokerlist='tcp://host:port?option=\'value\'&option=\'value\';vm://:3/virtualpath?option=\'value\''&failover='method?option=\'value\'&option='value''" + Options are of course optional except for requiring a single broker in the broker list. + The option seperator is defined to be either '&' or ',' + */ +public interface ConnectionURL +{ + public static final String AMQ_PROTOCOL = "amqp"; + public static final String OPTIONS_BROKERLIST = "brokerlist"; + public static final String OPTIONS_FAILOVER = "failover"; + public static final String OPTIONS_FAILOVER_CYCLE = "cyclecount"; + public static final String OPTIONS_SSL = "ssl"; + + String getURL(); + + String getFailoverMethod(); + + String getFailoverOption(String key); + + int getBrokerCount(); + + BrokerDetails getBrokerDetails(int index); + + void addBrokerDetails(BrokerDetails broker); + + List<BrokerDetails> getAllBrokerDetails(); + + String getClientName(); + + void setClientName(String clientName); + + String getUsername(); + + void setUsername(String username); + + String getPassword(); + + void setPassword(String password); + + String getVirtualHost(); + + void setVirtualHost(String virtualHost); + + String getOption(String key); + + void setOption(String key, String value); +} diff --git a/java/client/src/org/apache/qpid/jms/FailoverPolicy.java b/java/client/src/org/apache/qpid/jms/FailoverPolicy.java new file mode 100644 index 0000000000..a1a89f8a66 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/FailoverPolicy.java @@ -0,0 +1,306 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +import org.apache.qpid.jms.failover.FailoverMethod; +import org.apache.qpid.jms.failover.FailoverRoundRobinServers; +import org.apache.qpid.jms.failover.FailoverSingleServer; +import org.apache.log4j.Logger; + + +public class FailoverPolicy +{ + private static final Logger _logger = Logger.getLogger(FailoverPolicy.class); + + private static final long MINUTE = 60000L; + + private static final long DEFAULT_METHOD_TIMEOUT = 1 * MINUTE; + private static final long DEFAULT_FAILOVER_TIMEOUT = 4 * MINUTE; + + private FailoverMethod _methods[] = new FailoverMethod[1]; + + private int _currentMethod; + + private int _methodsRetries; + + private int _currentRetry; + + private boolean _timing; + + private long _lastMethodTime; + private long _lastFailTime; + + public FailoverPolicy(ConnectionURL connectionDetails) + { + FailoverMethod method; + + //todo This should be integrated in to the connection url when it supports + // multiple strategies. + + _methodsRetries = 0; + + if (connectionDetails.getFailoverMethod() == null) + { + if (connectionDetails.getBrokerCount() > 1) + { + method = new FailoverRoundRobinServers(connectionDetails); + } + else + { + method = new FailoverSingleServer(connectionDetails); + } + } + else + { + String failoverMethod = connectionDetails.getFailoverMethod(); + +/* + if (failoverMethod.equals(FailoverMethod.RANDOM)) + { + //todo write a random connection Failover + } +*/ + if (failoverMethod.equals(FailoverMethod.ROUND_ROBIN)) + { + method = new FailoverRoundRobinServers(connectionDetails); + } + else + { + try + { + Class[] constructorSpec = {ConnectionURL.class}; + Object [] params = {connectionDetails}; + + method = (FailoverMethod) ClassLoader.getSystemClassLoader(). + loadClass(failoverMethod). + getConstructor(constructorSpec).newInstance(params); + } + catch (Exception cnfe) + { + throw new IllegalArgumentException("Unknown failover method:" + failoverMethod); + } + } + } + + if (method == null) + { + throw new IllegalArgumentException("Unknown failover method specified."); + } + + reset(); + + _methods[_currentMethod] = method; + } + + public FailoverPolicy(FailoverMethod method) + { + this(method, 0); + } + + public FailoverPolicy(FailoverMethod method, int retries) + { + _methodsRetries = retries; + + reset(); + + _methods[_currentMethod] = method; + } + + private void reset() + { + _currentMethod = 0; + _currentRetry = 0; + _timing = false; + + } + + public boolean failoverAllowed() + { + boolean failoverAllowed; + + if (_timing) + { + long now = System.currentTimeMillis(); + + if ((now - _lastMethodTime) >= DEFAULT_METHOD_TIMEOUT) + { + _logger.info("Failover method timeout"); + _lastMethodTime = now; + + if (!nextMethod()) + { + return false; + } + + + } + else if ((now - _lastFailTime) >= DEFAULT_FAILOVER_TIMEOUT) + { + _logger.info("Failover timeout"); + return false; + } + else + { + _lastMethodTime = now; + } + } + else + { + _timing = true; + _lastMethodTime = System.currentTimeMillis(); + _lastFailTime = _lastMethodTime; + } + + + if (_methods[_currentMethod].failoverAllowed()) + { + failoverAllowed = true; + } + else + { + if (_currentMethod < (_methods.length - 1)) + { + nextMethod(); + _logger.info("Changing method to " + _methods[_currentMethod].methodName()); + return failoverAllowed(); + } + else + { + return cycleMethods(); + } + } + + return failoverAllowed; + } + + private boolean nextMethod() + { + if (_currentMethod < (_methods.length - 1)) + { + _currentMethod++; + _methods[_currentMethod].reset(); + return true; + } + else + { + return cycleMethods(); + } + } + + private boolean cycleMethods() + { + if (_currentRetry < _methodsRetries) + { + _currentRetry++; + + _currentMethod = 0; + + _logger.info("Retrying methods starting with " + _methods[_currentMethod].methodName()); + _methods[_currentMethod].reset(); + return failoverAllowed(); + } + else + { + _logger.debug("All failover methods exhausted"); + return false; + } + } + + /** + * Notification that connection was successful. + */ + public void attainedConnection() + { + _currentRetry = 0; + + _methods[_currentMethod].attainedConnection(); + + _timing = false; + } + + public BrokerDetails getCurrentBrokerDetails() + { + return _methods[_currentMethod].getCurrentBrokerDetails(); + } + + public BrokerDetails getNextBrokerDetails() + { + return _methods[_currentMethod].getNextBrokerDetails(); + } + + public void setBroker(BrokerDetails broker) + { + _methods[_currentMethod].setBroker(broker); + } + + public void addMethod(FailoverMethod method) + { + int len = _methods.length + 1; + FailoverMethod[] newMethods = new FailoverMethod[len]; + System.arraycopy(_methods, 0, newMethods, 0, _methods.length); + int index = len - 1; + newMethods[index] = method; + _methods = newMethods; + } + + public void setMethodRetries(int retries) + { + _methodsRetries = retries; + } + + public FailoverMethod getCurrentMethod() + { + if (_currentMethod >= 0 && _currentMethod < (_methods.length - 1)) + { + return _methods[_currentMethod]; + } + else + { + return null; + } + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + + sb.append("Failover Policy:\n"); + + if (failoverAllowed()) + { + sb.append("Failover allowed\n"); + } + else + { + sb.append("Failover not allowed\n"); + } + + sb.append("Failover policy methods\n"); + for (int i = 0; i < _methods.length; i++) + { + + if (i == _currentMethod) + { + sb.append(">"); + } + sb.append(_methods[i].toString()); + } + + return sb.toString(); + } +} diff --git a/java/client/src/org/apache/qpid/jms/MessageConsumer.java b/java/client/src/org/apache/qpid/jms/MessageConsumer.java new file mode 100644 index 0000000000..2116ebe918 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/MessageConsumer.java @@ -0,0 +1,24 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +/** + */ +public interface MessageConsumer extends javax.jms.MessageConsumer +{ +} diff --git a/java/client/src/org/apache/qpid/jms/MessageProducer.java b/java/client/src/org/apache/qpid/jms/MessageProducer.java new file mode 100644 index 0000000000..e00d271cc3 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/MessageProducer.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import java.io.UnsupportedEncodingException; + +/** + */ +public interface MessageProducer extends javax.jms.MessageProducer +{ + /** + * Set the default MIME type for messages produced by this producer. This reduces the overhead of each message. + * @param mimeType + */ + void setMimeType(String mimeType); + + /** + * Set the default encoding for messages produced by this producer. This reduces the overhead of each message. + * @param encoding the encoding as understood by XXXX how do I specify this?? RG + * @throws UnsupportedEncodingException if the encoding is not understood + */ + void setEncoding(String encoding) throws UnsupportedEncodingException; + + void send(Destination destination, Message message, int deliveryMode, + int priority, long timeToLive, boolean immediate) + throws JMSException; + + void send(Destination destination, Message message, int deliveryMode, + int priority, long timeToLive, boolean mandatory, boolean immediate) + throws JMSException; +} diff --git a/java/client/src/org/apache/qpid/jms/Session.java b/java/client/src/org/apache/qpid/jms/Session.java new file mode 100644 index 0000000000..82a2311498 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/Session.java @@ -0,0 +1,70 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms; + +import javax.jms.*; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; + + +public interface Session extends javax.jms.Session +{ + /** + * Indicates that no client acknowledgements are required. Broker assumes that once it has delivered + * a message packet successfully it is acknowledged. + */ + static final int NO_ACKNOWLEDGE = 257; + + /** + * Pre acknowledge means that an ack is sent per message but sent before user code has processed + * the message (i.e. before the onMessage() call or the receive() method has returned). + */ + static final int PRE_ACKNOWLEDGE = 258; + + MessageConsumer createConsumer(Destination destination, + int prefetch, + boolean noLocal, + boolean exclusive, + String selector) throws JMSException; + + /** + * @return the prefetch value used by default for consumers created on this session. + */ + int getDefaultPrefetch(); + + /** + * Create a producer + * @param destination + * @param mandatory the value of the mandatory flag used by default on the producer + * @param immediate the value of the immediate flag used by default on the producer + * @return + * @throws JMSException + */ + MessageProducer createProducer(Destination destination, boolean mandatory, boolean immediate) + throws JMSException; + + /** + * Create a producer + * @param destination + * @param immediate the value of the immediate flag used by default on the producer + * @return + * @throws JMSException + */ + MessageProducer createProducer(Destination destination, boolean immediate) + throws JMSException; +} diff --git a/java/client/src/org/apache/qpid/jms/failover/FailoverMethod.java b/java/client/src/org/apache/qpid/jms/failover/FailoverMethod.java new file mode 100644 index 0000000000..ff1336eb9d --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/failover/FailoverMethod.java @@ -0,0 +1,72 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms.failover; + +import org.apache.qpid.jms.BrokerDetails; + +public interface FailoverMethod +{ + public static final String ROUND_ROBIN = "roundrobin"; + public static final String RANDOM = "random"; + /** + * Reset the Failover to initial conditions + */ + void reset(); + + /** + * Check if failover is possible for this method + * + * @return true if failover is allowed + */ + boolean failoverAllowed(); + + /** + * Notification to the Failover method that a connection has been attained. + */ + void attainedConnection(); + + /** + * If there is no current BrokerDetails the null will be returned. + * @return The current BrokerDetail value to use + */ + BrokerDetails getCurrentBrokerDetails(); + + /** + * Move to the next BrokerDetails if one is available. + * @return the next BrokerDetail or null if there is none. + */ + BrokerDetails getNextBrokerDetails(); + + /** + * Set the currently active broker to be the new value. + * @param broker The new BrokerDetail value + */ + void setBroker(BrokerDetails broker); + + /** + * Set the retries for this method + * @param maxRetries the maximum number of time to retry this Method + */ + void setRetries(int maxRetries); + + /** + * @return The name of this method for display purposes. + */ + String methodName(); +} diff --git a/java/client/src/org/apache/qpid/jms/failover/FailoverRoundRobinServers.java b/java/client/src/org/apache/qpid/jms/failover/FailoverRoundRobinServers.java new file mode 100644 index 0000000000..715f605308 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/failover/FailoverRoundRobinServers.java @@ -0,0 +1,256 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms.failover; + +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.jms.ConnectionURL; +import org.apache.log4j.Logger; + +public class FailoverRoundRobinServers implements FailoverMethod +{ + private static final Logger _logger = Logger.getLogger(FailoverRoundRobinServers.class); + + /** The default number of times to cycle through all servers */ + public static final int DEFAULT_CYCLE_RETRIES = 0; + /** The default number of times to retry each server */ + public static final int DEFAULT_SERVER_RETRIES = 0; + + /** + * The index into the hostDetails array of the broker to which we are connected + */ + private int _currentBrokerIndex = -1; + + /** + * The number of times to retry connecting for each server + */ + private int _serverRetries; + + /** + * The current number of retry attempts made + */ + private int _currentServerRetry; + + /** + * The number of times to cycle through the servers + */ + private int _cycleRetries; + + /** + * The current number of cycles performed. + */ + private int _currentCycleRetries; + + /** + * Array of BrokerDetail used to make connections. + */ + private ConnectionURL _connectionDetails; + + public FailoverRoundRobinServers(ConnectionURL connectionDetails) + { + if (!(connectionDetails.getBrokerCount() > 0)) + { + throw new IllegalArgumentException("At least one broker details must be specified."); + } + + _connectionDetails = connectionDetails; + + //There is no current broker at startup so set it to -1. + _currentBrokerIndex = -1; + + String cycleRetries = _connectionDetails.getFailoverOption(ConnectionURL.OPTIONS_FAILOVER_CYCLE); + + if (cycleRetries != null) + { + try + { + _cycleRetries = Integer.parseInt(cycleRetries); + } + catch (NumberFormatException nfe) + { + _cycleRetries = DEFAULT_CYCLE_RETRIES; + } + } + + _currentCycleRetries = 0; + + _serverRetries = 0; + _currentServerRetry = -1; + } + + public void reset() + { + _currentBrokerIndex = 0; + _currentCycleRetries = 0; + _currentServerRetry = -1; + } + + public boolean failoverAllowed() + { + return ((_currentCycleRetries < _cycleRetries) + || (_currentServerRetry < _serverRetries) + || (_currentBrokerIndex < (_connectionDetails.getBrokerCount() - 1))); + } + + public void attainedConnection() + { + _currentCycleRetries = 0; + _currentServerRetry = -1; + } + + public BrokerDetails getCurrentBrokerDetails() + { + if (_currentBrokerIndex == -1) + { + return null; + } + + return _connectionDetails.getBrokerDetails(_currentBrokerIndex); + } + + + + public BrokerDetails getNextBrokerDetails() + { + if (_currentBrokerIndex == (_connectionDetails.getBrokerCount() - 1)) + { + if (_currentServerRetry < _serverRetries) + { + if (_currentBrokerIndex == -1) + { + _currentBrokerIndex = 0; + + setBroker(_connectionDetails.getBrokerDetails(_currentBrokerIndex )); + + _logger.info("First run using " + _connectionDetails.getBrokerDetails(_currentBrokerIndex)); + } + else + { + _logger.info("Retrying " + _connectionDetails.getBrokerDetails(_currentBrokerIndex)); + } + + _currentServerRetry++; + } + else + { + _currentCycleRetries++; + //failed to connect to first broker + _currentBrokerIndex = 0; + + setBroker(_connectionDetails.getBrokerDetails(_currentBrokerIndex )); + + // This is zero rather than -1 as we are already retrieving the details. + _currentServerRetry = 0; + } + //else - should force client to stop as max retries has been reached. + } + else + { + if (_currentServerRetry < _serverRetries) + { + if (_currentBrokerIndex == -1) + { + _currentBrokerIndex = 0; + + setBroker(_connectionDetails.getBrokerDetails(_currentBrokerIndex )); + + _logger.info("First run using " + _connectionDetails.getBrokerDetails(_currentBrokerIndex)); + } + else + { + _logger.info("Retrying " + _connectionDetails.getBrokerDetails(_currentBrokerIndex)); + } + _currentServerRetry++; + } + else + { + _currentBrokerIndex++; + + setBroker(_connectionDetails.getBrokerDetails(_currentBrokerIndex )); + // This is zero rather than -1 as we are already retrieving the details. + _currentServerRetry = 0; + } + } + + return _connectionDetails.getBrokerDetails(_currentBrokerIndex); + } + + + public void setBroker(BrokerDetails broker) + { + + _connectionDetails.addBrokerDetails(broker); + + int index = _connectionDetails.getAllBrokerDetails().indexOf(broker); + + String serverRetries = broker.getOption(BrokerDetails.OPTIONS_RETRY); + + if (serverRetries != null) + { + try + { + _serverRetries = Integer.parseInt(serverRetries); + } + catch (NumberFormatException nfe) + { + _serverRetries = DEFAULT_SERVER_RETRIES; + } + } + + _currentServerRetry = -1; + _currentBrokerIndex = index; + } + + public void setRetries(int maxRetries) + { + _cycleRetries = maxRetries; + } + + public String methodName() + { + return "Cycle Servers"; + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + + sb.append("Cycle Servers:\n"); + + sb.append("Cycle Retries:"); + sb.append(_cycleRetries); + sb.append("\nCurrent Cycle:"); + sb.append(_currentCycleRetries); + sb.append("\nServer Retries:"); + sb.append(_serverRetries); + sb.append("\nCurrent Retry:"); + sb.append(_currentServerRetry); + sb.append("\n"); + + for(int i=0; i < _connectionDetails.getBrokerCount() ; i++) + { + if (i == _currentBrokerIndex) + { + sb.append(">"); + } + sb.append(_connectionDetails.getBrokerDetails(i)); + sb.append("\n"); + } + + return sb.toString(); + } +} diff --git a/java/client/src/org/apache/qpid/jms/failover/FailoverSingleServer.java b/java/client/src/org/apache/qpid/jms/failover/FailoverSingleServer.java new file mode 100644 index 0000000000..76dc6a2dd5 --- /dev/null +++ b/java/client/src/org/apache/qpid/jms/failover/FailoverSingleServer.java @@ -0,0 +1,144 @@ +/* + * + * Copyright (c) 2006 The Apache Software Foundation + * + * Licensed 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.apache.qpid.jms.failover; + +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.jms.ConnectionURL; + +public class FailoverSingleServer implements FailoverMethod +{ + /** The default number of times to rety a conection to this server */ + public static final int DEFAULT_SERVER_RETRIES = 1; + + /** + * The details of the Single Server + */ + private BrokerDetails _brokerDetail; + + /** + * The number of times to retry connecting to the sever + */ + private int _retries; + + /** + * The current number of attempts made to the server + */ + private int _currentRetries; + + + public FailoverSingleServer(ConnectionURL connectionDetails) + { + if (connectionDetails.getBrokerCount() > 0) + { + setBroker(connectionDetails.getBrokerDetails(0)); + } + else + { + throw new IllegalArgumentException("BrokerDetails details required for connection."); + } + } + + public FailoverSingleServer(BrokerDetails brokerDetail) + { + setBroker(brokerDetail); + } + + public void reset() + { + _currentRetries = -1; + } + + public boolean failoverAllowed() + { + return _currentRetries < _retries; + } + + public void attainedConnection() + { + reset(); + } + + public BrokerDetails getCurrentBrokerDetails() + { + return _brokerDetail; + } + + public BrokerDetails getNextBrokerDetails() + { + if (_currentRetries == _retries) + { + return null; + } + else + { + if (_currentRetries < _retries) + { + _currentRetries ++; + } + + return _brokerDetail; + } + } + + public void setBroker(BrokerDetails broker) + { + if (broker == null) + { + throw new IllegalArgumentException("BrokerDetails details cannot be null"); + } + _brokerDetail = broker; + + String retries = broker.getOption(BrokerDetails.OPTIONS_RETRY); + if (retries != null) + { + try + { + _retries = Integer.parseInt(retries); + } + catch (NumberFormatException nfe) + { + _retries = DEFAULT_SERVER_RETRIES; + } + } + else + { + _retries = DEFAULT_SERVER_RETRIES; + } + + reset(); + } + + public void setRetries(int retries) + { + _retries = retries; + } + + public String methodName() + { + return "Single Server"; + } + + public String toString() + { + return "SingleServer:\n"+ + "Max Retries:"+_retries+ + "\nCurrent Retry:"+_currentRetries+ + "\n"+_brokerDetail+"\n"; + } + +} |
