From 1db5a8a2329ec064d1683294ee1a3d8d233de42d Mon Sep 17 00:00:00 2001 From: Stephen Vinoski Date: Sat, 18 Nov 2006 02:12:32 +0000 Subject: directory moves required for maven merge git-svn-id: https://svn.apache.org/repos/asf/incubator/qpid/trunk/qpid@476414 13f79535-47bb-0310-9956-ffa450edef68 --- .../qpid/client/AMQAuthenticationException.java | 32 + .../org/apache/qpid/client/AMQBrokerDetails.java | 304 +++++ .../java/org/apache/qpid/client/AMQConnection.java | 974 +++++++++++++++ .../apache/qpid/client/AMQConnectionFactory.java | 361 ++++++ .../org/apache/qpid/client/AMQConnectionURL.java | 402 ++++++ .../org/apache/qpid/client/AMQDestination.java | 285 +++++ .../org/apache/qpid/client/AMQHeadersExchange.java | 58 + .../qpid/client/AMQNoConsumersException.java | 37 + .../apache/qpid/client/AMQNoRouteException.java | 37 + .../main/java/org/apache/qpid/client/AMQQueue.java | 94 ++ .../apache/qpid/client/AMQQueueSessionAdaptor.java | 179 +++ .../java/org/apache/qpid/client/AMQSession.java | 1291 ++++++++++++++++++++ .../org/apache/qpid/client/AMQTemporaryQueue.java | 47 + .../org/apache/qpid/client/AMQTemporaryTopic.java | 49 + .../main/java/org/apache/qpid/client/AMQTopic.java | 92 ++ .../apache/qpid/client/AMQTopicSessionAdaptor.java | 169 +++ .../apache/qpid/client/BasicMessageConsumer.java | 535 ++++++++ .../apache/qpid/client/BasicMessageProducer.java | 484 ++++++++ .../java/org/apache/qpid/client/Closeable.java | 52 + .../qpid/client/ConnectionTuneParameters.java | 72 ++ .../qpid/client/JmsNotImplementedException.java | 31 + .../apache/qpid/client/QueueReceiverAdaptor.java | 86 ++ .../apache/qpid/client/TopicSubscriberAdaptor.java | 94 ++ .../qpid/client/failover/FailoverException.java | 33 + .../qpid/client/failover/FailoverHandler.java | 183 +++ .../apache/qpid/client/failover/FailoverState.java | 49 + .../qpid/client/failover/FailoverSupport.java | 66 + .../client/handler/BasicDeliverMethodHandler.java | 50 + .../client/handler/BasicReturnMethodHandler.java | 52 + .../client/handler/ChannelCloseMethodHandler.java | 82 ++ .../handler/ChannelCloseOkMethodHandler.java | 46 + .../client/handler/ChannelFlowOkMethodHandler.java | 49 + .../handler/ConnectionCloseMethodHandler.java | 92 ++ .../handler/ConnectionOpenOkMethodHandler.java | 55 + .../handler/ConnectionRedirectMethodHandler.java | 68 ++ .../handler/ConnectionSecureMethodHandler.java | 67 + .../handler/ConnectionStartMethodHandler.java | 187 +++ .../handler/ConnectionTuneMethodHandler.java | 82 ++ .../org/apache/qpid/client/message/AMQMessage.java | 71 ++ .../qpid/client/message/AbstractJMSMessage.java | 680 +++++++++++ .../client/message/AbstractJMSMessageFactory.java | 80 ++ .../qpid/client/message/JMSBytesMessage.java | 400 ++++++ .../client/message/JMSBytesMessageFactory.java | 40 + .../qpid/client/message/JMSObjectMessage.java | 178 +++ .../client/message/JMSObjectMessageFactory.java | 40 + .../apache/qpid/client/message/JMSTextMessage.java | 162 +++ .../qpid/client/message/JMSTextMessageFactory.java | 42 + .../apache/qpid/client/message/MessageFactory.java | 38 + .../client/message/MessageFactoryRegistry.java | 108 ++ .../message/UnexpectedBodyReceivedException.java | 43 + .../qpid/client/message/UnprocessedMessage.java | 64 + .../qpid/client/protocol/AMQMethodEvent.java | 62 + .../qpid/client/protocol/AMQMethodListener.java | 44 + .../qpid/client/protocol/AMQProtocolHandler.java | 553 +++++++++ .../qpid/client/protocol/AMQProtocolSession.java | 409 +++++++ .../protocol/BlockingMethodFrameListener.java | 136 +++ .../qpid/client/protocol/HeartbeatConfig.java | 60 + .../qpid/client/protocol/HeartbeatDiagnostics.java | 121 ++ .../protocol/ProtocolBufferMonitorFilter.java | 113 ++ .../qpid/client/security/AMQCallbackHandler.java | 30 + .../client/security/CallbackHandlerRegistry.java | 157 +++ .../security/CallbackHandlerRegistry.properties | 20 + .../qpid/client/security/DynamicSaslRegistrar.java | 128 ++ .../security/DynamicSaslRegistrar.properties | 19 + .../apache/qpid/client/security/JCAProvider.java | 46 + .../security/UsernamePasswordCallbackHandler.java | 56 + .../security/amqplain/AmqPlainSaslClient.java | 104 ++ .../amqplain/AmqPlainSaslClientFactory.java | 62 + .../org/apache/qpid/client/state/AMQState.java | 56 + .../qpid/client/state/AMQStateChangedEvent.java | 48 + .../apache/qpid/client/state/AMQStateListener.java | 26 + .../apache/qpid/client/state/AMQStateManager.java | 227 ++++ .../state/IllegalStateTransitionException.java | 48 + .../client/state/StateAwareMethodListener.java | 34 + .../apache/qpid/client/state/StateListener.java | 30 + .../org/apache/qpid/client/state/StateWaiter.java | 117 ++ .../listener/SpecificMethodFrameListener.java | 41 + .../AMQNoTransportForProtocolException.java | 53 + .../transport/AMQTransportConnectionException.java | 32 + .../client/transport/ITransportConnection.java | 33 + .../transport/SocketTransportConnection.java | 97 ++ .../qpid/client/transport/TransportConnection.java | 322 +++++ .../transport/VmPipeTransportConnection.java | 67 + .../client/util/FlowControllingBlockingQueue.java | 103 ++ .../vmbroker/AMQVMBrokerCreationException.java | 44 + .../java/org/apache/qpid/jms/BrokerDetails.java | 73 ++ .../qpid/jms/ChannelLimitReachedException.java | 46 + .../main/java/org/apache/qpid/jms/Connection.java | 69 ++ .../org/apache/qpid/jms/ConnectionListener.java | 58 + .../java/org/apache/qpid/jms/ConnectionURL.java | 72 ++ .../java/org/apache/qpid/jms/FailoverPolicy.java | 319 +++++ .../java/org/apache/qpid/jms/MessageConsumer.java | 27 + .../java/org/apache/qpid/jms/MessageProducer.java | 52 + .../src/main/java/org/apache/qpid/jms/Session.java | 90 ++ .../apache/qpid/jms/failover/FailoverMethod.java | 76 ++ .../jms/failover/FailoverRoundRobinServers.java | 261 ++++ .../qpid/jms/failover/FailoverSingleServer.java | 147 +++ .../java/org/apache/qpid/jndi/Example.properties | 39 + .../java/org/apache/qpid/jndi/NameParserImpl.java | 37 + .../jndi/PropertiesFileInitialContextFactory.java | 292 +++++ .../java/org/apache/qpid/jndi/ReadOnlyContext.java | 497 ++++++++ 101 files changed, 14525 insertions(+) create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQAuthenticationException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQBrokerDetails.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQConnection.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQConnectionFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQConnectionURL.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQDestination.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQHeadersExchange.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQNoConsumersException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQNoRouteException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQQueue.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQQueueSessionAdaptor.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQSession.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQTemporaryQueue.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQTemporaryTopic.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQTopic.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/AMQTopicSessionAdaptor.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/BasicMessageConsumer.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/BasicMessageProducer.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/Closeable.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/ConnectionTuneParameters.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/JmsNotImplementedException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/QueueReceiverAdaptor.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/TopicSubscriberAdaptor.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/failover/FailoverException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/failover/FailoverHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/failover/FailoverState.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/failover/FailoverSupport.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/BasicReturnMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/AMQMessage.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/AbstractJMSMessage.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/AbstractJMSMessageFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/JMSBytesMessage.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/JMSBytesMessageFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/JMSObjectMessage.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/JMSObjectMessageFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/JMSTextMessage.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/JMSTextMessageFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/MessageFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/MessageFactoryRegistry.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/message/UnprocessedMessage.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/AMQMethodEvent.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/AMQMethodListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/AMQProtocolHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/AMQProtocolSession.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/HeartbeatConfig.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/AMQCallbackHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/JCAProvider.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/AMQState.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/AMQStateChangedEvent.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/AMQStateListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/AMQStateManager.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/IllegalStateTransitionException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/StateAwareMethodListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/StateListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/StateWaiter.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/transport/AMQNoTransportForProtocolException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/transport/AMQTransportConnectionException.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/transport/ITransportConnection.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/transport/SocketTransportConnection.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/transport/TransportConnection.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/transport/VmPipeTransportConnection.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/util/FlowControllingBlockingQueue.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/vmbroker/AMQVMBrokerCreationException.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/BrokerDetails.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/ChannelLimitReachedException.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/Connection.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/ConnectionListener.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/ConnectionURL.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/FailoverPolicy.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/MessageConsumer.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/MessageProducer.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/Session.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/failover/FailoverMethod.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/failover/FailoverRoundRobinServers.java create mode 100644 java/client/src/main/java/org/apache/qpid/jms/failover/FailoverSingleServer.java create mode 100644 java/client/src/main/java/org/apache/qpid/jndi/Example.properties create mode 100644 java/client/src/main/java/org/apache/qpid/jndi/NameParserImpl.java create mode 100644 java/client/src/main/java/org/apache/qpid/jndi/PropertiesFileInitialContextFactory.java create mode 100644 java/client/src/main/java/org/apache/qpid/jndi/ReadOnlyContext.java (limited to 'java/client/src/main') diff --git a/java/client/src/main/java/org/apache/qpid/client/AMQAuthenticationException.java b/java/client/src/main/java/org/apache/qpid/client/AMQAuthenticationException.java new file mode 100644 index 0000000000..74462a8d19 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQAuthenticationException.java @@ -0,0 +1,32 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQBrokerDetails.java b/java/client/src/main/java/org/apache/qpid/client/AMQBrokerDetails.java new file mode 100644 index 0000000000..7cabc667c1 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQBrokerDetails.java @@ -0,0 +1,304 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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 _options; + + public AMQBrokerDetails() + { + _options = new HashMap(); + } + + 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 USE_SSL_DEFAULT; + } + + public void useSSL(boolean ssl) + { + setOption(OPTIONS_SSL, Boolean.toString(ssl)); + } + + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/AMQConnection.java b/java/client/src/main/java/org/apache/qpid/client/AMQConnection.java new file mode 100644 index 0000000000..98db26d0c4 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQConnection.java @@ -0,0 +1,974 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQConnectionException; +import org.apache.qpid.AMQException; +import org.apache.qpid.AMQUndeliveredException; +import org.apache.qpid.AMQUnresolvedAddressException; +import org.apache.qpid.client.failover.FailoverSupport; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.client.state.AMQState; +import org.apache.qpid.client.transport.TransportConnection; +import org.apache.qpid.framing.BasicQosBody; +import org.apache.qpid.framing.BasicQosOkBody; +import org.apache.qpid.framing.ChannelOpenBody; +import org.apache.qpid.framing.ChannelOpenOkBody; +import org.apache.qpid.framing.TxSelectBody; +import org.apache.qpid.framing.TxSelectOkBody; +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.jms.ChannelLimitReachedException; +import org.apache.qpid.jms.Connection; +import org.apache.qpid.jms.ConnectionListener; +import org.apache.qpid.jms.ConnectionURL; +import org.apache.qpid.jms.FailoverPolicy; +import org.apache.qpid.url.URLSyntaxException; + +import javax.jms.*; +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.naming.StringRefAddr; +import java.io.IOException; +import java.net.ConnectException; +import java.nio.channels.UnresolvedAddressException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +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 && checkException(lastException) && _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.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, _failoverPolicy.getCurrentBrokerDetails().toString()); + } + e.initCause(lastException); + } + + throw e; + } + } + + protected boolean checkException(Throwable thrown) + { + Throwable cause = thrown.getCause(); + + if (cause == null) + { + cause = thrown; + } + + return ((cause instanceof ConnectException) || (cause instanceof UnresolvedAddressException)); + } + + 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(brokerDetail).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_HIGH_MARK); + } + + public org.apache.qpid.jms.Session createSession(final boolean transacted, final int acknowledgeMode, + final int prefetch) throws JMSException + { + return createSession(transacted, acknowledgeMode, prefetch, prefetch); + } + + public org.apache.qpid.jms.Session createSession(final boolean transacted, final int acknowledgeMode, + final int prefetchHigh, final int prefetchLow) 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, + prefetchHigh, prefetchLow); + _protocolHandler.addSessionByChannel(channelId, session); + registerSession(channelId, session); + + boolean success = false; + try + { + createChannelOverWire(channelId, prefetchHigh, prefetchLow, 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 prefetchHigh, int prefetchLow, boolean transacted) + throws AMQException + { + _protocolHandler.syncWrite( + ChannelOpenBody.createAMQFrame(channelId, null), ChannelOpenOkBody.class); + + //todo send low water mark when protocol allows. + _protocolHandler.syncWrite( + BasicQosBody.createAMQFrame(channelId, 0, prefetchHigh, 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 prefetchHigh, int prefetchLow, boolean transacted) throws AMQException + { + try + { + createChannelOverWire(channelId, prefetchHigh, prefetchLow, 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; + } + + /** + * Returns an AMQQueueSessionAdaptor which wraps an AMQSession and throws IllegalStateExceptions + * where specified in the JMS spec + * @param transacted + * @param acknowledgeMode + * @return QueueSession + * @throws JMSException + */ + public QueueSession createQueueSession(boolean transacted, int acknowledgeMode) throws JMSException + { + return new AMQQueueSessionAdaptor(createSession(transacted, acknowledgeMode)); + } + + /** + * Returns an AMQTopicSessionAdapter which wraps an AMQSession and throws IllegalStateExceptions + * where specified in the JMS spec + * @param transacted + * @param acknowledgeMode + * @return TopicSession + * @throws JMSException + */ + public TopicSession createTopicSession(boolean transacted, int acknowledgeMode) throws JMSException + { + return new AMQTopicSessionAdaptor(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. + *

+ * 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 + *

+ * 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() throws JMSException + { + checkNotClosed(); + return _maximumChannelCount; + } + + public void setConnectionListener(ConnectionListener listener) + { + _connectionListener = listener; + } + + public ConnectionListener getConnectionListener() + { + return _connectionListener; + } + + public void setMaximumChannelCount(long maximumChannelCount) + { + _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 JMSException, 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.getDefaultPrefetchHigh(), s.getDefaultPrefetchLow(), 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/main/java/org/apache/qpid/client/AMQConnectionFactory.java b/java/client/src/main/java/org/apache/qpid/client/AMQConnectionFactory.java new file mode 100644 index 0000000000..6829769b69 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQConnectionFactory.java @@ -0,0 +1,361 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQConnectionURL.java b/java/client/src/main/java/org/apache/qpid/client/AMQConnectionURL.java new file mode 100644 index 0000000000..c134c2093b --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQConnectionURL.java @@ -0,0 +1,402 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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 _failoverOptions; + private HashMap _options; + private List _brokers; + private String _clientName; + private String _username; + private String _password; + private String _virtualHost; + + public AMQConnectionURL(String fullURL) throws URLSyntaxException + { + _url = fullURL; + _options = new HashMap(); + _brokers = new LinkedList(); + _failoverOptions = new HashMap(); + + // 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 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/main/java/org/apache/qpid/client/AMQDestination.java b/java/client/src/main/java/org/apache/qpid/client/AMQDestination.java new file mode 100644 index 0000000000..6401e3b23f --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQDestination.java @@ -0,0 +1,285 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQHeadersExchange.java b/java/client/src/main/java/org/apache/qpid/client/AMQHeadersExchange.java new file mode 100644 index 0000000000..c5bae6e1fa --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQHeadersExchange.java @@ -0,0 +1,58 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import org.apache.qpid.exchange.ExchangeDefaults; +import org.apache.qpid.url.BindingURL; + +/** + * A destination backed by a headers exchange + */ +public class AMQHeadersExchange extends AMQDestination +{ + public AMQHeadersExchange(BindingURL binding) + { + this(binding.getExchangeName()); + } + + 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/main/java/org/apache/qpid/client/AMQNoConsumersException.java b/java/client/src/main/java/org/apache/qpid/client/AMQNoConsumersException.java new file mode 100644 index 0000000000..277e3f7eaf --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQNoConsumersException.java @@ -0,0 +1,37 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQNoRouteException.java b/java/client/src/main/java/org/apache/qpid/client/AMQNoRouteException.java new file mode 100644 index 0000000000..0e84ad75f2 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQNoRouteException.java @@ -0,0 +1,37 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQQueue.java b/java/client/src/main/java/org/apache/qpid/client/AMQQueue.java new file mode 100644 index 0000000000..8304a29e4d --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQQueue.java @@ -0,0 +1,94 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQQueueSessionAdaptor.java b/java/client/src/main/java/org/apache/qpid/client/AMQQueueSessionAdaptor.java new file mode 100644 index 0000000000..05071bd2a8 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQQueueSessionAdaptor.java @@ -0,0 +1,179 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import javax.jms.*; +import javax.jms.IllegalStateException; +import java.io.Serializable; + +/** + * Need this adaptor class to conform to JMS spec and throw IllegalStateException + * from createDurableSubscriber, unsubscribe, createTopic & createTemporaryTopic + */ +public class AMQQueueSessionAdaptor implements QueueSession +{ + //holds a session for delegation + protected final AMQSession _session; + + /** + * Construct an adaptor with a session to wrap + * @param session + */ + public AMQQueueSessionAdaptor(Session session) + { + _session = (AMQSession) session; + } + + public TemporaryQueue createTemporaryQueue() throws JMSException { + return _session.createTemporaryQueue(); + } + + public Queue createQueue(String string) throws JMSException { + return _session.createQueue(string); + } + + public QueueReceiver createReceiver(Queue queue) throws JMSException { + return _session.createReceiver(queue); + } + + public QueueReceiver createReceiver(Queue queue, String string) throws JMSException { + return _session.createReceiver(queue, string); + } + + public QueueSender createSender(Queue queue) throws JMSException { + return _session.createSender(queue); + } + + public QueueBrowser createBrowser(Queue queue) throws JMSException { + return _session.createBrowser(queue); + } + + public QueueBrowser createBrowser(Queue queue, String string) throws JMSException { + return _session.createBrowser(queue, string); + } + + public BytesMessage createBytesMessage() throws JMSException { + return _session.createBytesMessage(); + } + + public MapMessage createMapMessage() throws JMSException { + return _session.createMapMessage(); + } + + public Message createMessage() throws JMSException { + return _session.createMessage(); + } + + public ObjectMessage createObjectMessage() throws JMSException { + return _session.createObjectMessage(); + } + + public ObjectMessage createObjectMessage(Serializable serializable) throws JMSException { + return _session.createObjectMessage(serializable); + } + + public StreamMessage createStreamMessage() throws JMSException { + return _session.createStreamMessage(); + } + + public TextMessage createTextMessage() throws JMSException { + return _session.createTextMessage(); + } + + public TextMessage createTextMessage(String string) throws JMSException { + return _session.createTextMessage(string); + } + + public boolean getTransacted() throws JMSException { + return _session.getTransacted(); + } + + public int getAcknowledgeMode() throws JMSException { + return _session.getAcknowledgeMode(); + } + + public void commit() throws JMSException { + _session.commit(); + } + + public void rollback() throws JMSException { + _session.rollback(); + } + + public void close() throws JMSException { + _session.close(); + } + + public void recover() throws JMSException { + _session.recover(); + } + + public MessageListener getMessageListener() throws JMSException { + return _session.getMessageListener(); + } + + public void setMessageListener(MessageListener messageListener) throws JMSException { + _session.setMessageListener(messageListener); + } + + public void run() { + _session.run(); + } + + public MessageProducer createProducer(Destination destination) throws JMSException { + return _session.createProducer(destination); + } + + public MessageConsumer createConsumer(Destination destination) throws JMSException { + return _session.createConsumer(destination); + } + + public MessageConsumer createConsumer(Destination destination, String string) throws JMSException { + return _session.createConsumer(destination,string); + } + + public MessageConsumer createConsumer(Destination destination, String string, boolean b) throws JMSException { + return _session.createConsumer(destination,string,b); + } + + //The following methods cannot be called from a QueueSession as per JMS spec + + public Topic createTopic(String string) throws JMSException { + throw new IllegalStateException("Cannot call createTopic from QueueSession"); + } + + public TopicSubscriber createDurableSubscriber(Topic topic, String string) throws JMSException { + throw new IllegalStateException("Cannot call createDurableSubscriber from QueueSession"); + } + + public TopicSubscriber createDurableSubscriber(Topic topic, String string, String string1, boolean b) throws JMSException { + throw new IllegalStateException("Cannot call createDurableSubscriber from QueueSession"); + } + + public TemporaryTopic createTemporaryTopic() throws JMSException { + throw new IllegalStateException("Cannot call createTemporaryTopic from QueueSession"); + } + + public void unsubscribe(String string) throws JMSException { + throw new IllegalStateException("Cannot call unsubscribe from QueueSession"); + } + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/AMQSession.java b/java/client/src/main/java/org/apache/qpid/client/AMQSession.java new file mode 100644 index 0000000000..a847658846 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQSession.java @@ -0,0 +1,1291 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import org.apache.log4j.Logger; +import org.apache.qpid.AMQException; +import org.apache.qpid.AMQUndeliveredException; +import org.apache.qpid.client.failover.FailoverSupport; +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.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.text.MessageFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AMQSession extends Closeable implements Session, QueueSession, TopicSession +{ + private static final Logger _logger = Logger.getLogger(AMQSession.class); + + public static final int DEFAULT_PREFETCH_HIGH_MARK = 5000; + public static final int DEFAULT_PREFETCH_LOW_MARK = 2500; + + private AMQConnection _connection; + + private boolean _transacted; + + private int _acknowledgeMode; + + private int _channelId; + + private int _defaultPrefetchHighMark = DEFAULT_PREFETCH_HIGH_MARK; + private int _defaultPrefetchLowMark = DEFAULT_PREFETCH_LOW_MARK; + + /** + * 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_HIGH_MARK, DEFAULT_PREFETCH_LOW_MARK); + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode, + MessageFactoryRegistry messageFactoryRegistry, int defaultPrefetch) + { + this(con, channelId, transacted, acknowledgeMode, messageFactoryRegistry, defaultPrefetch, defaultPrefetch); + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode, + MessageFactoryRegistry messageFactoryRegistry, int defaultPrefetchHighMark, int defaultPrefetchLowMark) + { + _connection = con; + _transacted = transacted; + if (transacted) + { + _acknowledgeMode = javax.jms.Session.SESSION_TRANSACTED; + } + else + { + _acknowledgeMode = acknowledgeMode; + } + _channelId = channelId; + _messageFactoryRegistry = messageFactoryRegistry; + _defaultPrefetchHighMark = defaultPrefetchHighMark; + _defaultPrefetchLowMark = defaultPrefetchLowMark; + + if (_acknowledgeMode == NO_ACKNOWLEDGE) + { + _queue = new FlowControllingBlockingQueue(_defaultPrefetchHighMark, _defaultPrefetchLowMark, + new FlowControllingBlockingQueue.ThresholdListener() + { + public void aboveThreshold(int currentValue) + { + if (_acknowledgeMode == NO_ACKNOWLEDGE) + { + _logger.warn("Above threshold(" + _defaultPrefetchHighMark + ") so suspending channel. Current value is " + currentValue); + suspendChannel(); + } + } + + public void underThreshold(int currentValue) + { + if (_acknowledgeMode == NO_ACKNOWLEDGE) + { + _logger.warn("Below threshold(" + _defaultPrefetchLowMark + ") so unsuspending channel. Current value is " + currentValue); + unsuspendChannel(); + } + } + }); + } + else + { + _queue = new FlowControllingBlockingQueue(_defaultPrefetchHighMark, null); + } + } + + 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); + } + + AMQSession(AMQConnection con, int channelId, boolean transacted, int acknowledgeMode, int defaultPrefetchHigh, int defaultPrefetchLow) + { + this(con, channelId, transacted, acknowledgeMode, MessageFactoryRegistry.newDefaultRegistry(), defaultPrefetchHigh, defaultPrefetchLow); + } + + 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 text 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 text message: " + e); + } + } + } + + public boolean getTransacted() throws JMSException + { + 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();) + { + //Sends acknowledgement to server + ((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 not 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 not 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() throws JMSException + { + checkNotClosed(); + + return new BasicMessageProducer(_connection, (AMQDestination) destination, _transacted, _channelId, + AMQSession.this, _connection.getProtocolHandler(), + getNextProducerId(), immediate, mandatory, waitUntilSent); + } + }.execute(_connection); + } + + /** + * Creates a QueueReceiver + * @param destination + * @return QueueReceiver - a wrapper around our MessageConsumer + * @throws JMSException + */ + public QueueReceiver createQueueReceiver(Destination destination) throws JMSException + { + AMQQueue dest = (AMQQueue) destination; + BasicMessageConsumer consumer = (BasicMessageConsumer) createConsumer(destination); + return new QueueReceiverAdaptor(dest, consumer); + } + + /** + * Creates a QueueReceiver using a message selector + * @param destination + * @param messageSelector + * @return QueueReceiver - a wrapper around our MessageConsumer + * @throws JMSException + */ + public QueueReceiver createQueueReceiver(Destination destination, String messageSelector) throws JMSException + { + AMQQueue dest = (AMQQueue) destination; + BasicMessageConsumer consumer = (BasicMessageConsumer) + createConsumer(destination, messageSelector); + return new QueueReceiverAdaptor(dest, consumer); + } + + public MessageConsumer createConsumer(Destination destination) throws JMSException + { + return createConsumer(destination, _defaultPrefetchHighMark, _defaultPrefetchLowMark, false, false, null); + } + + public MessageConsumer createConsumer(Destination destination, String messageSelector) throws JMSException + { + return createConsumer(destination, _defaultPrefetchHighMark, _defaultPrefetchLowMark, false, false, messageSelector); + } + + public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean noLocal) + throws JMSException + { + return createConsumer(destination, _defaultPrefetchHighMark, _defaultPrefetchLowMark, noLocal, false, messageSelector); + } + + public MessageConsumer createConsumer(Destination destination, + int prefetch, + boolean noLocal, + boolean exclusive, + String selector) throws JMSException + { + return createConsumer(destination, prefetch, prefetch, noLocal, exclusive, selector, null); + } + + + public MessageConsumer createConsumer(Destination destination, + int prefetchHigh, + int prefetchLow, + boolean noLocal, + boolean exclusive, + String selector) throws JMSException + { + return createConsumer(destination, prefetchHigh, prefetchLow, 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, prefetch, noLocal, exclusive, + selector, rawSelector); + } + + public MessageConsumer createConsumer(Destination destination, + int prefetchHigh, + int prefetchLow, + boolean noLocal, + boolean exclusive, + String selector, + FieldTable rawSelector) throws JMSException + { + return createConsumerImpl(destination, prefetchHigh, prefetchLow, noLocal, exclusive, + selector, rawSelector); + } + + protected MessageConsumer createConsumerImpl(final Destination destination, + final int prefetchHigh, + final int prefetchLow, + 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, prefetchHigh, prefetchLow, 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()); + } + + public void declareExchangeSynch(String name, String type) throws AMQException + { + AMQFrame frame = ExchangeDeclareBody.createAMQFrame(_channelId, 0, name, type, false, false, false, false, false, null); + _connection.getProtocolHandler().syncWrite(frame, ExchangeDeclareOkBody.class); + } + + 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 prefetchHigh, int prefetchLow, + boolean noLocal, boolean exclusive, int acknowledgeMode) throws AMQException + { + //fixme prefetch values are not used here. Do we need to have them as parametsrs? + //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; + } + } + } + + /** + * Creates a QueueReceiver wrapping a MessageConsumer + * @param queue + * @return QueueReceiver + * @throws JMSException + */ + public QueueReceiver createReceiver(Queue queue) throws JMSException + { + AMQQueue dest = (AMQQueue) queue; + BasicMessageConsumer consumer = (BasicMessageConsumer) createConsumer(dest); + return new QueueReceiverAdaptor(dest, consumer); + } + + /** + * Creates a QueueReceiver wrapping a MessageConsumer using a message selector + * @param queue + * @param messageSelector + * @return QueueReceiver + * @throws JMSException + */ + public QueueReceiver createReceiver(Queue queue, String messageSelector) throws JMSException + { + AMQQueue dest = (AMQQueue) queue; + BasicMessageConsumer consumer = (BasicMessageConsumer) + createConsumer(dest, messageSelector); + return new QueueReceiverAdaptor(dest, consumer); + } + + 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; + } + } + } + + /** + * Creates a non-durable subscriber + * @param topic + * @return TopicSubscriber - a wrapper round our MessageConsumer + * @throws JMSException + */ + public TopicSubscriber createSubscriber(Topic topic) throws JMSException + { + AMQTopic dest = new AMQTopic(topic.getTopicName()); + return new TopicSubscriberAdaptor(dest, (BasicMessageConsumer) createConsumer(dest)); + } + + /** + * Creates a non-durable subscriber with a message selector + * @param topic + * @param messageSelector + * @param noLocal + * @return TopicSubscriber - a wrapper round our MessageConsumer + * @throws JMSException + */ + public TopicSubscriber createSubscriber(Topic topic, String messageSelector, boolean noLocal) throws JMSException + { + AMQTopic dest = new AMQTopic(topic.getTopicName()); + return new TopicSubscriberAdaptor(dest, (BasicMessageConsumer) createConsumer(dest, 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 _defaultPrefetchHighMark; + } + + public int getDefaultPrefetchHigh() + { + return _defaultPrefetchHighMark; + } + + public int getDefaultPrefetchLow() + { + return _defaultPrefetchLowMark; + } + + 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.getPrefetchHigh(), consumer.getPrefetchLow(), + 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/main/java/org/apache/qpid/client/AMQTemporaryQueue.java b/java/client/src/main/java/org/apache/qpid/client/AMQTemporaryQueue.java new file mode 100644 index 0000000000..0b12bfb728 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQTemporaryQueue.java @@ -0,0 +1,47 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQTemporaryTopic.java b/java/client/src/main/java/org/apache/qpid/client/AMQTemporaryTopic.java new file mode 100644 index 0000000000..0ba5cb3c3a --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQTemporaryTopic.java @@ -0,0 +1,49 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQTopic.java b/java/client/src/main/java/org/apache/qpid/client/AMQTopic.java new file mode 100644 index 0000000000..89727f65b7 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQTopic.java @@ -0,0 +1,92 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/AMQTopicSessionAdaptor.java b/java/client/src/main/java/org/apache/qpid/client/AMQTopicSessionAdaptor.java new file mode 100644 index 0000000000..73613b6923 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/AMQTopicSessionAdaptor.java @@ -0,0 +1,169 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import javax.jms.*; +import javax.jms.IllegalStateException; +import java.io.Serializable; + +public class AMQTopicSessionAdaptor implements TopicSession +{ + protected final AMQSession _session; + + public AMQTopicSessionAdaptor(Session session) + { + _session = (AMQSession) session; + } + + public Topic createTopic(String string) throws JMSException { + return _session.createTopic(string); + } + + public TopicSubscriber createSubscriber(Topic topic) throws JMSException { + return _session.createSubscriber(topic); + } + + public TopicSubscriber createSubscriber(Topic topic, String string, boolean b) throws JMSException { + return _session.createSubscriber(topic, string, b); + } + + public TopicSubscriber createDurableSubscriber(Topic topic, String string) throws JMSException { + return _session.createDurableSubscriber(topic, string); + } + + public TopicSubscriber createDurableSubscriber(Topic topic, String string, String string1, boolean b) throws JMSException { + return _session.createDurableSubscriber(topic, string, string1, b); + } + + public TopicPublisher createPublisher(Topic topic) throws JMSException { + return _session.createPublisher(topic); + } + + public TemporaryTopic createTemporaryTopic() throws JMSException { + return _session.createTemporaryTopic(); + } + + public void unsubscribe(String string) throws JMSException { + _session.unsubscribe(string); + } + + public BytesMessage createBytesMessage() throws JMSException { + return _session.createBytesMessage(); + } + + public MapMessage createMapMessage() throws JMSException { + return _session.createMapMessage(); + } + + public Message createMessage() throws JMSException { + return _session.createMessage(); + } + + public ObjectMessage createObjectMessage() throws JMSException { + return _session.createObjectMessage(); + } + + public ObjectMessage createObjectMessage(Serializable serializable) throws JMSException { + return _session.createObjectMessage(); + } + + public StreamMessage createStreamMessage() throws JMSException { + return _session.createStreamMessage(); + } + + public TextMessage createTextMessage() throws JMSException { + return _session.createTextMessage(); + } + + public TextMessage createTextMessage(String string) throws JMSException { + return _session.createTextMessage(); + } + + public boolean getTransacted() throws JMSException { + return _session.getTransacted(); + } + + public int getAcknowledgeMode() throws JMSException { + return _session.getAcknowledgeMode(); + } + + public void commit() throws JMSException { + _session.commit(); + } + + public void rollback() throws JMSException { + _session.rollback(); + } + + public void close() throws JMSException { + _session.close(); + } + + public void recover() throws JMSException { + _session.recover(); + } + + public MessageListener getMessageListener() throws JMSException { + return _session.getMessageListener(); + } + + public void setMessageListener(MessageListener messageListener) throws JMSException { + _session.setMessageListener(messageListener); + } + + public void run() { + _session.run(); + } + + public MessageProducer createProducer(Destination destination) throws JMSException { + return _session.createProducer(destination); + } + + public MessageConsumer createConsumer(Destination destination) throws JMSException { + return _session.createConsumer(destination); + } + + public MessageConsumer createConsumer(Destination destination, String string) throws JMSException { + return _session.createConsumer(destination, string); + } + + public MessageConsumer createConsumer(Destination destination, String string, boolean b) throws JMSException { + return _session.createConsumer(destination, string, b); + } + + //The following methods cannot be called from a TopicSession as per JMS spec + public Queue createQueue(String string) throws JMSException { + throw new IllegalStateException("Cannot call createQueue from TopicSession"); + } + + public QueueBrowser createBrowser(Queue queue) throws JMSException { + throw new IllegalStateException("Cannot call createBrowser from TopicSession"); + } + + public QueueBrowser createBrowser(Queue queue, String string) throws JMSException { + throw new IllegalStateException("Cannot call createBrowser from TopicSession"); + } + + public TemporaryQueue createTemporaryQueue() throws JMSException { + throw new IllegalStateException("Cannot call createTemporaryQueue from TopicSession"); + } + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/BasicMessageConsumer.java b/java/client/src/main/java/org/apache/qpid/client/BasicMessageConsumer.java new file mode 100644 index 0000000000..f97ea6bf1e --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/BasicMessageConsumer.java @@ -0,0 +1,535 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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 high water prefetch field in order to be able to reuse it when resubscribing in the event of failover + */ + private int _prefetchHigh; + + /** + * We store the low water prefetch field in order to be able to reuse it when resubscribing in the event of failover + */ + private int _prefetchLow; + + /** + * 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; + + /** + * Switch to enable sending of acknowledgements when using DUPS_OK_ACKNOWLEDGE mode. + * Enabled when _outstannding number of msgs >= _prefetchHigh and disabled at < _prefetchLow + */ + private boolean _dups_ok_acknowledge_send; + + protected BasicMessageConsumer(int channelId, AMQConnection connection, AMQDestination destination, String messageSelector, + boolean noLocal, MessageFactoryRegistry messageFactory, AMQSession session, + AMQProtocolHandler protocolHandler, FieldTable rawSelectorFieldTable, + int prefetchHigh, int prefetchLow, boolean exclusive, int acknowledgeMode) + { + _channelId = channelId; + _connection = connection; + _messageSelector = messageSelector; + _noLocal = noLocal; + _destination = destination; + _messageFactory = messageFactory; + _session = session; + _protocolHandler = protocolHandler; + _rawSelectorFieldTable = rawSelectorFieldTable; + _prefetchHigh = prefetchHigh; + _prefetchLow = prefetchLow; + _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 _prefetchHigh; + } + + public int getPrefetchHigh() + { + return _prefetchHigh; + } + + public int getPrefetchLow() + { + return _prefetchLow; + } + + 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 >= _prefetchHigh) + { + _dups_ok_acknowledge_send = true; + } + if (_outstanding <= _prefetchLow) + { + _dups_ok_acknowledge_send = false; + } + + if (_dups_ok_acknowledge_send) + { + _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/main/java/org/apache/qpid/client/BasicMessageProducer.java b/java/client/src/main/java/org/apache/qpid/client/BasicMessageProducer.java new file mode 100644 index 0000000000..14cafc3558 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/BasicMessageProducer.java @@ -0,0 +1,484 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import org.apache.log4j.Logger; +import org.apache.mina.common.ByteBuffer; +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 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) throws JMSException + { + checkNotClosed(); + _mimeType = mimeType; + } + + public void setEncoding(String encoding) throws JMSException, UnsupportedEncodingException + { + checkNotClosed(); + _encoding = encoding; + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/Closeable.java b/java/client/src/main/java/org/apache/qpid/client/Closeable.java new file mode 100644 index 0000000000..873cb55726 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/Closeable.java @@ -0,0 +1,52 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import javax.jms.IllegalStateException; +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() throws JMSException + { + 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/main/java/org/apache/qpid/client/ConnectionTuneParameters.java b/java/client/src/main/java/org/apache/qpid/client/ConnectionTuneParameters.java new file mode 100644 index 0000000000..b1ec7216bc --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/ConnectionTuneParameters.java @@ -0,0 +1,72 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/JmsNotImplementedException.java b/java/client/src/main/java/org/apache/qpid/client/JmsNotImplementedException.java new file mode 100644 index 0000000000..903514c35f --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/JmsNotImplementedException.java @@ -0,0 +1,31 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import javax.jms.JMSException; + +public class JmsNotImplementedException extends JMSException +{ + public JmsNotImplementedException() + { + super("Not implemented"); + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/QueueReceiverAdaptor.java b/java/client/src/main/java/org/apache/qpid/client/QueueReceiverAdaptor.java new file mode 100644 index 0000000000..57e458d833 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/QueueReceiverAdaptor.java @@ -0,0 +1,86 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client; + +import javax.jms.*; + +/** + * Class that wraps a MessageConsumer for backwards JMS compatibility + * Returned by methods in AMQSession etc + */ +public class QueueReceiverAdaptor implements QueueReceiver { + + protected MessageConsumer _consumer; + protected Queue _queue; + + protected QueueReceiverAdaptor(Queue queue, MessageConsumer consumer) + { + _consumer = consumer; + _queue = queue; + } + + 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(); + } + + /** + * Return the queue associated with this receiver + * @return + * @throws JMSException + */ + public Queue getQueue() throws JMSException + { + return _queue; + } + + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/TopicSubscriberAdaptor.java b/java/client/src/main/java/org/apache/qpid/client/TopicSubscriberAdaptor.java new file mode 100644 index 0000000000..c776a9943e --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/TopicSubscriberAdaptor.java @@ -0,0 +1,94 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/failover/FailoverException.java b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverException.java new file mode 100644 index 0000000000..49377fdc19 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverException.java @@ -0,0 +1,33 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/failover/FailoverHandler.java b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverHandler.java new file mode 100644 index 0000000000..53291a3fd0 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverHandler.java @@ -0,0 +1,183 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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. + *

+ * 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/main/java/org/apache/qpid/client/failover/FailoverState.java b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverState.java new file mode 100644 index 0000000000..e5787439ec --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverState.java @@ -0,0 +1,49 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/failover/FailoverSupport.java b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverSupport.java new file mode 100644 index 0000000000..bb7397f194 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/failover/FailoverSupport.java @@ -0,0 +1,66 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java new file mode 100644 index 0000000000..6e12899204 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/BasicDeliverMethodHandler.java @@ -0,0 +1,50 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/BasicReturnMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/BasicReturnMethodHandler.java new file mode 100644 index 0000000000..8785a96396 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/BasicReturnMethodHandler.java @@ -0,0 +1,52 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java new file mode 100644 index 0000000000..2bd93f1508 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ChannelCloseMethodHandler.java @@ -0,0 +1,82 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java new file mode 100644 index 0000000000..383cebbcab --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ChannelCloseOkMethodHandler.java @@ -0,0 +1,46 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java new file mode 100644 index 0000000000..9a4f7af849 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ChannelFlowOkMethodHandler.java @@ -0,0 +1,49 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java new file mode 100644 index 0000000000..dd9fd651c1 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionCloseMethodHandler.java @@ -0,0 +1,92 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java new file mode 100644 index 0000000000..8785e7d44e --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionOpenOkMethodHandler.java @@ -0,0 +1,55 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java new file mode 100644 index 0000000000..a658e3e787 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionRedirectMethodHandler.java @@ -0,0 +1,68 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java new file mode 100644 index 0000000000..fd15faf429 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionSecureMethodHandler.java @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java new file mode 100644 index 0000000000..caef9a3f44 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionStartMethodHandler.java @@ -0,0 +1,187 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java new file mode 100644 index 0000000000..8fee277392 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/handler/ConnectionTuneMethodHandler.java @@ -0,0 +1,82 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/AMQMessage.java b/java/client/src/main/java/org/apache/qpid/client/message/AMQMessage.java new file mode 100644 index 0000000000..edabed90b3 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/AMQMessage.java @@ -0,0 +1,71 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/AbstractJMSMessage.java b/java/client/src/main/java/org/apache/qpid/client/message/AbstractJMSMessage.java new file mode 100644 index 0000000000..5d0a30f949 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/AbstractJMSMessage.java @@ -0,0 +1,680 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.message; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.mina.common.ByteBuffer; +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.client.JmsNotImplementedException; +import org.apache.qpid.framing.BasicContentHeaderProperties; +import org.apache.qpid.framing.FieldTable; +import org.apache.qpid.framing.FieldTableKeyEnumeration; + +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 JmsNotImplementedException(); + } + + public void setJMSDestination(Destination destination) throws JMSException + { + throw new JmsNotImplementedException(); + } + + 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 JmsNotImplementedException(); + } + + 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(""); + } + 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(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.BYTE_PROPERTY_PREFIX: + buf.append(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.SHORT_PROPERTY_PREFIX: + buf.append(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.INT_PROPERTY_PREFIX: + buf.append(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.LONG_PROPERTY_PREFIX: + buf.append(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.FLOAT_PROPERTY_PREFIX: + buf.append(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.DOUBLE_PROPERTY_PREFIX: + buf.append(" "); + break; + case org.apache.qpid.client.message.AbstractJMSMessage.STRING_PROPERTY_PREFIX: + buf.append(" "); + break; + default: + buf.append("= bytes.length ? bytes.length : _data.remaining()); + if (count == 0) + { + return -1; + } + else + { + _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()); + if (count == 0) + { + return -1; + } + else + { + _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/main/java/org/apache/qpid/client/message/JMSBytesMessageFactory.java b/java/client/src/main/java/org/apache/qpid/client/message/JMSBytesMessageFactory.java new file mode 100644 index 0000000000..6baf5326e5 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/JMSBytesMessageFactory.java @@ -0,0 +1,40 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/JMSObjectMessage.java b/java/client/src/main/java/org/apache/qpid/client/message/JMSObjectMessage.java new file mode 100644 index 0000000000..3c0d9b99c4 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/JMSObjectMessage.java @@ -0,0 +1,178 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/JMSObjectMessageFactory.java b/java/client/src/main/java/org/apache/qpid/client/message/JMSObjectMessageFactory.java new file mode 100644 index 0000000000..9d4a11650d --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/JMSObjectMessageFactory.java @@ -0,0 +1,40 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/JMSTextMessage.java b/java/client/src/main/java/org/apache/qpid/client/message/JMSTextMessage.java new file mode 100644 index 0000000000..4983f6b623 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/JMSTextMessage.java @@ -0,0 +1,162 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/JMSTextMessageFactory.java b/java/client/src/main/java/org/apache/qpid/client/message/JMSTextMessageFactory.java new file mode 100644 index 0000000000..d554794fdd --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/JMSTextMessageFactory.java @@ -0,0 +1,42 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/MessageFactory.java b/java/client/src/main/java/org/apache/qpid/client/message/MessageFactory.java new file mode 100644 index 0000000000..1fb178a1a4 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/MessageFactory.java @@ -0,0 +1,38 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/MessageFactoryRegistry.java b/java/client/src/main/java/org/apache/qpid/client/message/MessageFactoryRegistry.java new file mode 100644 index 0000000000..e6709adb0d --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/MessageFactoryRegistry.java @@ -0,0 +1,108 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java b/java/client/src/main/java/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java new file mode 100644 index 0000000000..735ddc54e8 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/UnexpectedBodyReceivedException.java @@ -0,0 +1,43 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/message/UnprocessedMessage.java b/java/client/src/main/java/org/apache/qpid/client/message/UnprocessedMessage.java new file mode 100644 index 0000000000..7d20c32b66 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/message/UnprocessedMessage.java @@ -0,0 +1,64 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/protocol/AMQMethodEvent.java b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQMethodEvent.java new file mode 100644 index 0000000000..403aa3486e --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQMethodEvent.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/protocol/AMQMethodListener.java b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQMethodListener.java new file mode 100644 index 0000000000..68299033a5 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQMethodListener.java @@ -0,0 +1,44 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/protocol/AMQProtocolHandler.java b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQProtocolHandler.java new file mode 100644 index 0000000000..eab9084717 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQProtocolHandler.java @@ -0,0 +1,553 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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.AMQConnectionClosedException; +import org.apache.qpid.AMQDisconnectedException; +import org.apache.qpid.AMQException; +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.AMQDataBlock; +import org.apache.qpid.framing.AMQFrame; +import org.apache.qpid.framing.AMQMethodBody; +import org.apache.qpid.framing.ConnectionCloseBody; +import org.apache.qpid.framing.ConnectionCloseOkBody; +import org.apache.qpid.framing.ContentBody; +import org.apache.qpid.framing.ContentHeaderBody; +import org.apache.qpid.framing.HeartbeatBody; +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) + { + //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 + { + 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 (_logger.isDebugEnabled()) + { + _logger.debug(_connection.getFailoverPolicy().toString()); + } + + 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 + { + try + { + _frameListeners.add(listener); + _protocolSession.writeFrame(frame); + + AMQMethodEvent e = listener.blockForFrame(); + return e; + // When control resumes before this line, a reply will have been received + // that matches the criteria defined in the blocking listener + } + finally + { + // If we don't remove the listener then no-one will + _frameListeners.remove(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/main/java/org/apache/qpid/client/protocol/AMQProtocolSession.java b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQProtocolSession.java new file mode 100644 index 0000000000..a4ed89719b --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/AMQProtocolSession.java @@ -0,0 +1,409 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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 org.apache.commons.lang.StringUtils; + +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 +{ + + protected static final int LAST_WRITE_FUTURE_JOIN_TIMEOUT = 1000 * 60 * 2; + + protected static final Logger _logger = Logger.getLogger(AMQProtocolSession.class); + + public static final String PROTOCOL_INITIATION_RECEIVED = "ProtocolInitiatiionReceived"; + + protected static final String CONNECTION_TUNE_PARAMETERS = "ConnectionTuneParameters"; + + protected static final String AMQ_CONNECTION = "AMQConnection"; + + protected static final String SASL_CLIENT = "SASLClient"; + + protected final IoSession _minaProtocolSession; + + protected WriteFuture _lastWriteFuture; + + /** + * The handler from which this session was created and which is used to handle protocol events. + * We send failover events to the handler. + */ + protected final AMQProtocolHandler _protocolHandler; + + /** + * Maps from the channel id to the AMQSession that it represents. + */ + protected ConcurrentMap _channelId2SessionMap = new ConcurrentHashMap(); + + protected 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. + */ + protected ConcurrentMap _channelId2UnprocessedMsgMap = new ConcurrentHashMap(); + + /** + * Counter to ensure unique queue names + */ + protected int _queueId = 1; + protected final Object _queueIdLock = new Object(); + + /** + * No-arg constructor for use by test subclass - has to initialise final vars + * NOT intended for use other then for test + */ + public AMQProtocolSession() + { + _protocolHandler = null; + _minaProtocolSession = null; + } + + 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) + { + writeFrame(frame, false); + } + + public void writeFrame(AMQDataBlock frame, boolean wait) + { + WriteFuture f = _minaProtocolSession.write(frame); + if (wait) + { + f.join(); + } + else + { + _lastWriteFuture = f; + } + } + + 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("Waiting for last write to join."); + if (_lastWriteFuture != null) + { + _lastWriteFuture.join(LAST_WRITE_FUTURE_JOIN_TIMEOUT); + } + + _logger.debug("Closing protocol session"); + final CloseFuture future = _minaProtocolSession.close(); + future.join(); + } + + public void failover(String host, int port) + { + _protocolHandler.failover(host, port); + } + + protected String generateQueueName() + { + int id; + synchronized(_queueIdLock) + { + id = _queueId++; + } + //get rid of / and : and ; from address for spec conformance + String localAddress = StringUtils.replaceChars(_minaProtocolSession.getLocalAddress().toString(),"/;:",""); + return "tmp_" + localAddress + "_" + 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/main/java/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java b/java/client/src/main/java/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java new file mode 100644 index 0000000000..492571b6af --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/BlockingMethodFrameListener.java @@ -0,0 +1,136 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/protocol/HeartbeatConfig.java b/java/client/src/main/java/org/apache/qpid/client/protocol/HeartbeatConfig.java new file mode 100644 index 0000000000..6a7462cd0f --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/HeartbeatConfig.java @@ -0,0 +1,60 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java b/java/client/src/main/java/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java new file mode 100644 index 0000000000..d44faeab04 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/HeartbeatDiagnostics.java @@ -0,0 +1,121 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java b/java/client/src/main/java/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java new file mode 100644 index 0000000000..e70d2ac661 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/protocol/ProtocolBufferMonitorFilter.java @@ -0,0 +1,113 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/security/AMQCallbackHandler.java b/java/client/src/main/java/org/apache/qpid/client/security/AMQCallbackHandler.java new file mode 100644 index 0000000000..72d8bedc06 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/AMQCallbackHandler.java @@ -0,0 +1,30 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.java b/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.java new file mode 100644 index 0000000000..9b3b83dc7a --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.java @@ -0,0 +1,157 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties b/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties new file mode 100644 index 0000000000..50e6f1efaa --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +CallbackHandler.CRAM-MD5=org.apache.qpid.client.security.UsernamePasswordCallbackHandler +CallbackHandler.PLAIN=org.apache.qpid.client.security.UsernamePasswordCallbackHandler diff --git a/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.java b/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.java new file mode 100644 index 0000000000..c21dc9e659 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.java @@ -0,0 +1,128 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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> 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> parseProperties(Properties props) + { + Enumeration e = props.propertyNames(); + TreeMap> factoriesToRegister = + new TreeMap>(); + 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) clazz); + } + catch (Exception ex) + { + _logger.error("Error instantiating SaslClientFactory calss " + className + " - skipping"); + } + } + return factoriesToRegister; + } + + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties b/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties new file mode 100644 index 0000000000..ee66664455 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +AMQPLAIN=org.apache.qpid.client.security.amqplain.AmqPlainSaslClientFactory diff --git a/java/client/src/main/java/org/apache/qpid/client/security/JCAProvider.java b/java/client/src/main/java/org/apache/qpid/client/security/JCAProvider.java new file mode 100644 index 0000000000..9a3b7bf7ab --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/JCAProvider.java @@ -0,0 +1,46 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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> 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> providerMap) + { + for (Map.Entry> me : + providerMap.entrySet()) + { + put("SaslClientFactory." + me.getKey(), me.getValue().getName()); + } + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java b/java/client/src/main/java/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java new file mode 100644 index 0000000000..7192601b18 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/UsernamePasswordCallbackHandler.java @@ -0,0 +1,56 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java b/java/client/src/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java new file mode 100644 index 0000000000..81d3fb76d5 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClient.java @@ -0,0 +1,104 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java b/java/client/src/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java new file mode 100644 index 0000000000..03d765de70 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/amqplain/AmqPlainSaslClientFactory.java @@ -0,0 +1,62 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/AMQState.java b/java/client/src/main/java/org/apache/qpid/client/state/AMQState.java new file mode 100644 index 0000000000..4996f59345 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/AMQState.java @@ -0,0 +1,56 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/AMQStateChangedEvent.java b/java/client/src/main/java/org/apache/qpid/client/state/AMQStateChangedEvent.java new file mode 100644 index 0000000000..edef54ccd6 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/AMQStateChangedEvent.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/AMQStateListener.java b/java/client/src/main/java/org/apache/qpid/client/state/AMQStateListener.java new file mode 100644 index 0000000000..110471aad0 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/AMQStateListener.java @@ -0,0 +1,26 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.state; + +public interface AMQStateListener +{ + void stateChanged(AMQStateChangedEvent evt); +} diff --git a/java/client/src/main/java/org/apache/qpid/client/state/AMQStateManager.java b/java/client/src/main/java/org/apache/qpid/client/state/AMQStateManager.java new file mode 100644 index 0000000000..ab707bb51d --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/AMQStateManager.java @@ -0,0 +1,227 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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. + *

+ * 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/main/java/org/apache/qpid/client/state/IllegalStateTransitionException.java b/java/client/src/main/java/org/apache/qpid/client/state/IllegalStateTransitionException.java new file mode 100644 index 0000000000..bd1145da9f --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/IllegalStateTransitionException.java @@ -0,0 +1,48 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/StateAwareMethodListener.java b/java/client/src/main/java/org/apache/qpid/client/state/StateAwareMethodListener.java new file mode 100644 index 0000000000..37a0f9f376 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/StateAwareMethodListener.java @@ -0,0 +1,34 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/StateListener.java b/java/client/src/main/java/org/apache/qpid/client/state/StateListener.java new file mode 100644 index 0000000000..df207a0a23 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/StateListener.java @@ -0,0 +1,30 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/StateWaiter.java b/java/client/src/main/java/org/apache/qpid/client/state/StateWaiter.java new file mode 100644 index 0000000000..42ae5a7b17 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/StateWaiter.java @@ -0,0 +1,117 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java b/java/client/src/main/java/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java new file mode 100644 index 0000000000..480d73e59e --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/state/listener/SpecificMethodFrameListener.java @@ -0,0 +1,41 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/transport/AMQNoTransportForProtocolException.java b/java/client/src/main/java/org/apache/qpid/client/transport/AMQNoTransportForProtocolException.java new file mode 100644 index 0000000000..93c17520b0 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/transport/AMQNoTransportForProtocolException.java @@ -0,0 +1,53 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.transport; + +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.AMQException; + +public class AMQNoTransportForProtocolException extends AMQTransportConnectionException +{ + BrokerDetails _details; + + public AMQNoTransportForProtocolException(BrokerDetails details) + { + this(details, "No Transport exists for specified broker protocol"); + } + + public AMQNoTransportForProtocolException(BrokerDetails details, String message) + { + super(message); + + _details = details; + } + + public String toString() + { + if (_details != null) + { + return super.toString() + _details.toString(); + } + else + { + return super.toString(); + } + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/transport/AMQTransportConnectionException.java b/java/client/src/main/java/org/apache/qpid/client/transport/AMQTransportConnectionException.java new file mode 100644 index 0000000000..4b17661bc3 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/transport/AMQTransportConnectionException.java @@ -0,0 +1,32 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.transport; + +import org.apache.qpid.AMQException; + +public class AMQTransportConnectionException extends AMQException +{ + public AMQTransportConnectionException(String message) + { + super(message); + + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/transport/ITransportConnection.java b/java/client/src/main/java/org/apache/qpid/client/transport/ITransportConnection.java new file mode 100644 index 0000000000..7630f59115 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/transport/ITransportConnection.java @@ -0,0 +1,33 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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/main/java/org/apache/qpid/client/transport/SocketTransportConnection.java b/java/client/src/main/java/org/apache/qpid/client/transport/SocketTransportConnection.java new file mode 100644 index 0000000000..94eb1b3d7a --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/transport/SocketTransportConnection.java @@ -0,0 +1,97 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.transport; + +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.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.setPreferDirectBuffers(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(); + // 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) + { + ioConnector.setThreadModel(new ReadWriteThreadModel()); + } + + SocketSessionConfig scfg = (SocketSessionConfig) ioConnector.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); + ioConnector.setHandler(protocolHandler); + ConnectFuture future = ioConnector.connect(address); + + // 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/main/java/org/apache/qpid/client/transport/TransportConnection.java b/java/client/src/main/java/org/apache/qpid/client/transport/TransportConnection.java new file mode 100644 index 0000000000..5bb975b503 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/transport/TransportConnection.java @@ -0,0 +1,322 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.transport; + +import org.apache.log4j.Logger; +import org.apache.mina.common.IoConnector; +import org.apache.mina.common.IoHandlerAdapter; + +import org.apache.mina.transport.vmpipe.VmPipeAcceptor; +import org.apache.mina.transport.vmpipe.VmPipeAddress; +import org.apache.mina.transport.socket.nio.SocketConnector; +import org.apache.qpid.client.AMQBrokerDetails; +import org.apache.qpid.jms.BrokerDetails; +import org.apache.qpid.pool.ReadWriteThreadModel; +import org.apache.qpid.client.vmbroker.AMQVMBrokerCreationException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 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; + + private static Map _inVmPipeAddress = new HashMap(); + private static VmPipeAcceptor _acceptor; + private static int _currentInstance = -1; + private static int _currentVMPort = -1; + + private static final int TCP = 0; + private static final int VM = 1; + + private static Logger _logger = Logger.getLogger(TransportConnection.class); + + private static final String DEFAULT_QPID_SERVER = "org.apache.qpid.server.protocol.AMQPFastProtocolHandler"; + + static + { + _acceptor = new VmPipeAcceptor(); + + _acceptor.setThreadModel(new ReadWriteThreadModel()); + } + + public static ITransportConnection getInstance() throws AMQTransportConnectionException + { + AMQBrokerDetails details = new AMQBrokerDetails(); + details.setTransport(BrokerDetails.TCP); + return getInstance(details); + } + + public static ITransportConnection getInstance(BrokerDetails details) throws AMQTransportConnectionException + { + int transport = getTransport(details.getTransport()); + + if (transport == -1) + + { + throw new AMQNoTransportForProtocolException(details); + } + + if (transport == _currentInstance) + + { + if (transport == VM) + { + if (_currentVMPort == details.getPort()) + { + return _instance; + } + } + else + { + return _instance; + } + } + + _currentInstance = transport; + + switch (transport) + + { + case TCP: + _instance = new SocketTransportConnection(new SocketTransportConnection.SocketConnectorFactory() + { + public IoConnector newSocketConnector() + { + SocketConnector result; + //FIXME - this needs to be sorted to use the new Mina MultiThread SA. + if (Boolean.getBoolean("qpidnio")) + { + _logger.warn("Using Qpid NIO - DISABLED"); +// result = new org.apache.qpid.nio.SocketConnector(); // non-blocking connector + } +// else + { + _logger.warn("Using Mina NIO"); + 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(0); + + return result; + } + }); + break; + case VM: + { + _instance = getVMTransport(details, Boolean.getBoolean("amqj.noAutoCreateVMBroker")); + break; + } + } + + return _instance; + } + + private static int getTransport(String transport) + { + if (transport.equals(BrokerDetails.TCP)) + { + return TCP; + } + + if (transport.equals(BrokerDetails.VM)) + { + return VM; + } + + return -1; + } + + private static ITransportConnection getVMTransport(BrokerDetails details, boolean noAutoCreate) throws AMQVMBrokerCreationException + { + int port = details.getPort(); + + if (!_inVmPipeAddress.containsKey(port)) + { + if (noAutoCreate) + { + throw new AMQVMBrokerCreationException(port, "VM Broker on port " + port + " does not exist. Auto create disabled."); + + } + else + { + _logger.info("Auto Creating VMBroker on port " + port); + createVMBroker(port); + } + + } + + return new VmPipeTransportConnection(port); + } + + + public static void createVMBroker(int port) throws AMQVMBrokerCreationException + { + + + if (!_inVmPipeAddress.containsKey(port)) + { + _logger.info("Creating InVM Qpid.AMQP listening on port " + port); + IoHandlerAdapter provider = null; + try + { + VmPipeAddress pipe = new VmPipeAddress(port); + + provider = createBrokerInstance(port); + + _acceptor.setLocalAddress(pipe); + _acceptor.setHandler(provider); + _acceptor.bind(); + + _inVmPipeAddress.put(port, pipe); + _logger.info("Created InVM Qpid.AMQP listening on port " + port); + } + catch (IOException e) + { + _logger.error(e); + + //Try and unbind provider + try + { + VmPipeAddress pipe = new VmPipeAddress(port); + + try + { + _acceptor.unbind(); + } + catch (Exception ignore) + { + //ignore + } + + if (provider == null) + { + provider = createBrokerInstance(port); + } + + _acceptor.setLocalAddress(pipe); + _acceptor.setHandler(provider); + _acceptor.bind(); + _inVmPipeAddress.put(port, _acceptor); + _logger.info("Created InVM Qpid.AMQP listening on port " + port); + } + catch (IOException justUseFirstException) + { + String because; + if (e.getCause() == null) + { + because = e.toString(); + } + else + { + because = e.getCause().toString(); + } + + throw new AMQVMBrokerCreationException(port, because + " Stopped binding of InVM Qpid.AMQP"); + } + } + } + else + { + _logger.info("InVM Qpid.AMQP on port " + port + " already exits."); + } + + } + + private static IoHandlerAdapter createBrokerInstance(int port) throws AMQVMBrokerCreationException + { + String protocolProviderClass = System.getProperty("amqj.protocolprovider.class", DEFAULT_QPID_SERVER); + _logger.info("Creating Qpid protocol provider: " + protocolProviderClass); + + // can't use introspection to get Provider as it is a server class. + // need to go straight to IoHandlerAdapter but that requries the queues and exchange from the ApplicationRegistry which we can't access. + + //get right constructor and pass in instancec ID - "port" + IoHandlerAdapter provider; + try + { + Class[] cnstr = {Integer.class}; + Object[] params = {port}; + provider = (IoHandlerAdapter) Class.forName(protocolProviderClass).getConstructor(cnstr).newInstance(params); + //Give the broker a second to create + _logger.info("Created Instance"); + } + catch (Exception e) + { + _logger.info("Unable to create InVM Qpid.AMQP on port " + port + ". Because: " + e.getCause()); + _logger.error(e); + String because; + if (e.getCause() == null) + { + because = e.toString(); + } + else + { + because = e.getCause().toString(); + } + + + throw new AMQVMBrokerCreationException(port, because + " Stopped InVM Qpid.AMQP creation"); + } + + return provider; + } + + public static void killAllVMBrokers() + { + _logger.info("Killing all VM Brokers"); + + Iterator keys = _inVmPipeAddress.keySet().iterator(); + + while (keys.hasNext()) + { + int id = (Integer) keys.next(); + + ((VmPipeAcceptor)_inVmPipeAddress.remove(id)).unbind(); + } + + } + + public static void killVMBroker(int port) + { + VmPipeAddress pipe = (VmPipeAddress) _inVmPipeAddress.get(port); + if (pipe != null) + { + _logger.info("Killing VM Broker:" + port); + _inVmPipeAddress.remove(port); + _acceptor.unbind(); + } + } + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/transport/VmPipeTransportConnection.java b/java/client/src/main/java/org/apache/qpid/client/transport/VmPipeTransportConnection.java new file mode 100644 index 0000000000..b871759428 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/transport/VmPipeTransportConnection.java @@ -0,0 +1,67 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.transport; + +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.client.protocol.AMQProtocolHandler; +import org.apache.qpid.client.transport.ITransportConnection; +import org.apache.qpid.pool.PoolingFilter; +import org.apache.qpid.pool.ReferenceCountingExecutorService; +import org.apache.qpid.jms.BrokerDetails; +import org.apache.log4j.Logger; +import org.apache.mina.common.ConnectFuture; +import org.apache.mina.transport.vmpipe.VmPipeAddress; +import org.apache.mina.transport.vmpipe.VmPipeConnector; + +import java.io.IOException; + +public class VmPipeTransportConnection implements ITransportConnection +{ + private static final Logger _logger = Logger.getLogger(VmPipeTransportConnection.class); + + private static int _port; + + public VmPipeTransportConnection(int port) + { + _port = port; + } + + public void connect(AMQProtocolHandler protocolHandler, BrokerDetails brokerDetail) throws IOException + { + final VmPipeConnector ioConnector = new VmPipeConnector(); + ReferenceCountingExecutorService executorService = ReferenceCountingExecutorService.getInstance(); + PoolingFilter asyncRead = new PoolingFilter(executorService, PoolingFilter.READ_EVENTS, + "AsynchronousReadFilter"); + ioConnector.getFilterChain().addFirst("AsynchronousReadFilter", asyncRead); + PoolingFilter asyncWrite = new PoolingFilter(executorService, PoolingFilter.WRITE_EVENTS, + "AsynchronousWriteFilter"); + ioConnector.getFilterChain().addLast("AsynchronousWriteFilter", asyncWrite); + + final VmPipeAddress address = new VmPipeAddress(_port); + _logger.info("Attempting connection to " + address); + ioConnector.setHandler(protocolHandler); + ConnectFuture future = ioConnector.connect(address); + // wait for connection to complete + future.join(); + // we call getSession which throws an IOException if there has been an error connecting + future.getSession(); + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/util/FlowControllingBlockingQueue.java b/java/client/src/main/java/org/apache/qpid/client/util/FlowControllingBlockingQueue.java new file mode 100644 index 0000000000..5b7144b524 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/util/FlowControllingBlockingQueue.java @@ -0,0 +1,103 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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. + *

+ * This implementation is only 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 _flowControlHighThreshold; + private final int _flowControlLowThreshold; + + 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) + { + this(threshold, threshold, listener); + } + + public FlowControllingBlockingQueue(int highThreshold, int lowThreshold, ThresholdListener listener) + { + _flowControlHighThreshold = highThreshold; + _flowControlLowThreshold = lowThreshold; + _listener = listener; + } + + public Object take() throws InterruptedException + { + Object o = _queue.take(); + if (_listener != null) + { + synchronized(_listener) + { + if (_count-- == _flowControlLowThreshold) + { + _listener.underThreshold(_count); + } + } + } + return o; + } + + public void add(Object o) + { + _queue.add(o); + if (_listener != null) + { + synchronized(_listener) + { + if (++_count == _flowControlHighThreshold) + { + _listener.aboveThreshold(_count); + } + } + } + } +} + diff --git a/java/client/src/main/java/org/apache/qpid/client/vmbroker/AMQVMBrokerCreationException.java b/java/client/src/main/java/org/apache/qpid/client/vmbroker/AMQVMBrokerCreationException.java new file mode 100644 index 0000000000..607ddcc26a --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/vmbroker/AMQVMBrokerCreationException.java @@ -0,0 +1,44 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.client.vmbroker; + +import org.apache.qpid.client.transport.AMQTransportConnectionException; + +public class AMQVMBrokerCreationException extends AMQTransportConnectionException +{ + private int _port; + + public AMQVMBrokerCreationException(int port) + { + this(port, "Unable to create vm broker"); + } + + public AMQVMBrokerCreationException(int port, String message) + { + super(message); + _port = port; + } + + public String toString() + { + return super.toString() + " on port " + _port; + } +} diff --git a/java/client/src/main/java/org/apache/qpid/jms/BrokerDetails.java b/java/client/src/main/java/org/apache/qpid/jms/BrokerDetails.java new file mode 100644 index 0000000000..293ce5e82e --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/jms/BrokerDetails.java @@ -0,0 +1,73 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.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 TCP = "tcp"; + public static final String VM = "vm"; + + public static final String DEFAULT_TRANSPORT = TCP; + + public static final String URL_FORMAT_EXAMPLE = + "://[:][?

+ * Based on class from ActiveMQ. + */ +public class NameParserImpl implements NameParser +{ + public Name parse(String name) throws NamingException + { + return new CompositeName(name); + } +} diff --git a/java/client/src/main/java/org/apache/qpid/jndi/PropertiesFileInitialContextFactory.java b/java/client/src/main/java/org/apache/qpid/jndi/PropertiesFileInitialContextFactory.java new file mode 100644 index 0000000000..b0641350ad --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/jndi/PropertiesFileInitialContextFactory.java @@ -0,0 +1,292 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.jndi; + +import org.apache.log4j.Logger; +import org.apache.qpid.client.AMQConnectionFactory; +import org.apache.qpid.client.AMQHeadersExchange; +import org.apache.qpid.client.AMQQueue; +import org.apache.qpid.client.AMQTopic; +import org.apache.qpid.exchange.ExchangeDefaults; +import org.apache.qpid.url.AMQBindingURL; +import org.apache.qpid.url.BindingURL; +import org.apache.qpid.url.URLSyntaxException; + +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import javax.jms.Queue; +import javax.jms.Topic; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PropertiesFileInitialContextFactory implements InitialContextFactory +{ + protected final Logger _logger = Logger.getLogger(getClass()); + + private String CONNECTION_FACTORY_PREFIX = "connectionfactory."; + private String DESTINATION_PREFIX = "destination."; + private String QUEUE_PREFIX = "queue."; + private String TOPIC_PREFIX = "topic."; + + public Context getInitialContext(Hashtable environment) throws NamingException + { + Map data = new ConcurrentHashMap(); + + createConnectionFactories(data, environment); + + createDestinations(data, environment); + + createQueues(data, environment); + + createTopics(data, environment); + + return createContext(data, environment); + } + + // Implementation methods + //------------------------------------------------------------------------- + protected ReadOnlyContext createContext(Map data, Hashtable environment) + { + return new ReadOnlyContext(environment, data); + } + + protected void createConnectionFactories(Map data, Hashtable environment) + { + for (Iterator iter = environment.entrySet().iterator(); iter.hasNext();) + { + Map.Entry entry = (Map.Entry) iter.next(); + String key = entry.getKey().toString(); + if (key.startsWith(CONNECTION_FACTORY_PREFIX)) + { + String jndiName = key.substring(CONNECTION_FACTORY_PREFIX.length()); + ConnectionFactory cf = createFactory(entry.getValue().toString()); + if (cf != null) + { + data.put(jndiName, cf); + } + } + } + } + + protected void createDestinations(Map data, Hashtable environment) + { + for (Iterator iter = environment.entrySet().iterator(); iter.hasNext();) + { + Map.Entry entry = (Map.Entry) iter.next(); + String key = entry.getKey().toString(); + if (key.startsWith(DESTINATION_PREFIX)) + { + String jndiName = key.substring(DESTINATION_PREFIX.length()); + Destination dest = createDestination(entry.getValue().toString()); + if (dest != null) + { + data.put(jndiName, dest); + } + } + } + } + + protected void createQueues(Map data, Hashtable environment) + { + for (Iterator iter = environment.entrySet().iterator(); iter.hasNext();) + { + Map.Entry entry = (Map.Entry) iter.next(); + String key = entry.getKey().toString(); + if (key.startsWith(QUEUE_PREFIX)) + { + String jndiName = key.substring(QUEUE_PREFIX.length()); + Queue q = createQueue(entry.getValue().toString()); + if (q != null) + { + data.put(jndiName, q); + } + } + } + } + + protected void createTopics(Map data, Hashtable environment) + { + for (Iterator iter = environment.entrySet().iterator(); iter.hasNext();) + { + Map.Entry entry = (Map.Entry) iter.next(); + String key = entry.getKey().toString(); + if (key.startsWith(TOPIC_PREFIX)) + { + String jndiName = key.substring(TOPIC_PREFIX.length()); + Topic t = createTopic(entry.getValue().toString()); + if (t != null) + { + data.put(jndiName, t); + } + } + } + } + + /** + * Factory method to create new Connection Factory instances + */ + protected ConnectionFactory createFactory(String url) + { + try + { + return new AMQConnectionFactory(url); + } + catch (URLSyntaxException urlse) + { + _logger.warn("Unable to createFactories:" + urlse); + } + return null; + } + + /** + * Factory method to create new Destination instances from an AMQP BindingURL + */ + protected Destination createDestination(String bindingURL) + { + AMQBindingURL binding; + try + { + binding = new AMQBindingURL(bindingURL); + } + catch (URLSyntaxException urlse) + { + _logger.warn("Unable to destination:" + urlse); + return null; + } + + if (binding.getExchangeClass().equals(ExchangeDefaults.TOPIC_EXCHANGE_CLASS)) + { + return createTopic(binding); + } + else if (binding.getExchangeClass().equals(ExchangeDefaults.DIRECT_EXCHANGE_CLASS)) + { + return createQueue(binding); + } + else if (binding.getExchangeClass().equals(ExchangeDefaults.HEADERS_EXCHANGE_CLASS)) + { + return createHeaderExchange(binding); + } + + _logger.warn("Binding: '" + binding + "' not supported"); + return null; + } + + /** + * Factory method to create new Queue instances + */ + protected Queue createQueue(Object value) + { + if (value instanceof String) + + { + return new AMQQueue((String) value); + } + else if (value instanceof BindingURL) + + { + return new AMQQueue((BindingURL) value); + } + + return null; + } + + /** + * Factory method to create new Topic instances + */ + protected Topic createTopic(Object value) + { + if (value instanceof String) + { + return new AMQTopic((String) value); + } + else if (value instanceof BindingURL) + + { + return new AMQTopic((BindingURL) value); + } + + return null; + } + + /** + * Factory method to create new HeaderExcahnge instances + */ + protected Destination createHeaderExchange(Object value) + { + if (value instanceof String) + { + return new AMQHeadersExchange((String) value); + } + else if (value instanceof BindingURL) + { + return new AMQHeadersExchange((BindingURL) value); + } + + return null; + } + + // Properties + //------------------------------------------------------------------------- + public String getConnectionPrefix() + { + return CONNECTION_FACTORY_PREFIX; + } + + public void setConnectionPrefix(String connectionPrefix) + { + this.CONNECTION_FACTORY_PREFIX = connectionPrefix; + } + + public String getDestinationPrefix() + { + return DESTINATION_PREFIX; + } + + public void setDestinationPrefix(String destinationPrefix) + { + this.DESTINATION_PREFIX = destinationPrefix; + } + + public String getQueuePrefix() + { + return QUEUE_PREFIX; + } + + public void setQueuePrefix(String queuePrefix) + { + this.QUEUE_PREFIX = queuePrefix; + } + + public String getTopicPrefix() + { + return TOPIC_PREFIX; + } + + public void setTopicPrefix(String topicPrefix) + { + this.TOPIC_PREFIX = topicPrefix; + } +} diff --git a/java/client/src/main/java/org/apache/qpid/jndi/ReadOnlyContext.java b/java/client/src/main/java/org/apache/qpid/jndi/ReadOnlyContext.java new file mode 100644 index 0000000000..1e96e6cf35 --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/jndi/ReadOnlyContext.java @@ -0,0 +1,497 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.qpid.jndi; + +import javax.naming.*; +import javax.naming.spi.NamingManager; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; + +/** + * Based on class from ActiveMQ. + * A read-only Context + *

+ * This version assumes it and all its subcontext are read-only and any attempt + * to modify (e.g. through bind) will result in an OperationNotSupportedException. + * Each Context in the tree builds a cache of the entries in all sub-contexts + * to optimise the performance of lookup. + *

+ *

This implementation is intended to optimise the performance of lookup(String) + * to about the level of a HashMap get. It has been observed that the scheme + * resolution phase performed by the JVM takes considerably longer, so for + * optimum performance lookups should be coded like:

+ * + * Context componentContext = (Context)new InitialContext().lookup("java:comp"); + * String envEntry = (String) componentContext.lookup("env/myEntry"); + * String envEntry2 = (String) componentContext.lookup("env/myEntry2"); + * + * + * @version $Revision$ $Date$ + */ +public class ReadOnlyContext implements Context, Serializable +{ + private static final long serialVersionUID = -5754338187296859149L; + protected static final NameParser nameParser = new NameParserImpl(); + + protected final Hashtable environment; // environment for this context + protected final Map bindings; // bindings at my level + protected final Map treeBindings; // all bindings under me + + private boolean frozen = false; + private String nameInNamespace = ""; + public static final String SEPARATOR = "/"; + + public ReadOnlyContext() + { + environment = new Hashtable(); + bindings = new HashMap(); + treeBindings = new HashMap(); + } + + public ReadOnlyContext(Hashtable env) + { + if (env == null) + { + this.environment = new Hashtable(); + } + else + { + this.environment = new Hashtable(env); + } + this.bindings = Collections.EMPTY_MAP; + this.treeBindings = Collections.EMPTY_MAP; + } + + public ReadOnlyContext(Hashtable environment, Map bindings) + { + if (environment == null) + { + this.environment = new Hashtable(); + } + else + { + this.environment = new Hashtable(environment); + } + this.bindings = bindings; + treeBindings = new HashMap(); + frozen = true; + } + + public ReadOnlyContext(Hashtable environment, Map bindings, String nameInNamespace) + { + this(environment, bindings); + this.nameInNamespace = nameInNamespace; + } + + protected ReadOnlyContext(ReadOnlyContext clone, Hashtable env) + { + this.bindings = clone.bindings; + this.treeBindings = clone.treeBindings; + this.environment = new Hashtable(env); + } + + protected ReadOnlyContext(ReadOnlyContext clone, Hashtable env, String nameInNamespace) + { + this(clone, env); + this.nameInNamespace = nameInNamespace; + } + + public void freeze() + { + frozen = true; + } + + boolean isFrozen() + { + return frozen; + } + + /** + * internalBind is intended for use only during setup or possibly by suitably synchronized superclasses. + * It binds every possible lookup into a map in each context. To do this, each context + * strips off one name segment and if necessary creates a new context for it. Then it asks that context + * to bind the remaining name. It returns a map containing all the bindings from the next context, plus + * the context it just created (if it in fact created it). (the names are suitably extended by the segment + * originally lopped off). + * + * @param name + * @param value + * @return + * @throws javax.naming.NamingException + */ + protected Map internalBind(String name, Object value) throws NamingException + { + assert name != null && name.length() > 0; + assert!frozen; + + Map newBindings = new HashMap(); + int pos = name.indexOf('/'); + if (pos == -1) + { + if (treeBindings.put(name, value) != null) + { + throw new NamingException("Something already bound at " + name); + } + bindings.put(name, value); + newBindings.put(name, value); + } + else + { + String segment = name.substring(0, pos); + assert segment != null; + assert!segment.equals(""); + Object o = treeBindings.get(segment); + if (o == null) + { + o = newContext(); + treeBindings.put(segment, o); + bindings.put(segment, o); + newBindings.put(segment, o); + } + else if (!(o instanceof ReadOnlyContext)) + { + throw new NamingException("Something already bound where a subcontext should go"); + } + ReadOnlyContext readOnlyContext = (ReadOnlyContext) o; + String remainder = name.substring(pos + 1); + Map subBindings = readOnlyContext.internalBind(remainder, value); + for (Iterator iterator = subBindings.entrySet().iterator(); iterator.hasNext();) + { + Map.Entry entry = (Map.Entry) iterator.next(); + String subName = segment + "/" + (String) entry.getKey(); + Object bound = entry.getValue(); + treeBindings.put(subName, bound); + newBindings.put(subName, bound); + } + } + return newBindings; + } + + protected ReadOnlyContext newContext() + { + return new ReadOnlyContext(); + } + + public Object addToEnvironment(String propName, Object propVal) throws NamingException + { + return environment.put(propName, propVal); + } + + public Hashtable getEnvironment() throws NamingException + { + return (Hashtable) environment.clone(); + } + + public Object removeFromEnvironment(String propName) throws NamingException + { + return environment.remove(propName); + } + + public Object lookup(String name) throws NamingException + { + if (name.length() == 0) + { + return this; + } + Object result = treeBindings.get(name); + if (result == null) + { + result = bindings.get(name); + } + if (result == null) + { + int pos = name.indexOf(':'); + if (pos > 0) + { + String scheme = name.substring(0, pos); + Context ctx = NamingManager.getURLContext(scheme, environment); + if (ctx == null) + { + throw new NamingException("scheme " + scheme + " not recognized"); + } + return ctx.lookup(name); + } + else + { + // Split out the first name of the path + // and look for it in the bindings map. + CompositeName path = new CompositeName(name); + + if (path.size() == 0) + { + return this; + } + else + { + String first = path.get(0); + Object obj = bindings.get(first); + if (obj == null) + { + throw new NameNotFoundException(name); + } + else if (obj instanceof Context && path.size() > 1) + { + Context subContext = (Context) obj; + obj = subContext.lookup(path.getSuffix(1)); + } + return obj; + } + } + } + if (result instanceof LinkRef) + { + LinkRef ref = (LinkRef) result; + result = lookup(ref.getLinkName()); + } + if (result instanceof Reference) + { + try + { + result = NamingManager.getObjectInstance(result, null, null, this.environment); + } + catch (NamingException e) + { + throw e; + } + catch (Exception e) + { + throw(NamingException) new NamingException("could not look up : " + name).initCause(e); + } + } + if (result instanceof ReadOnlyContext) + { + String prefix = getNameInNamespace(); + if (prefix.length() > 0) + { + prefix = prefix + SEPARATOR; + } + result = new ReadOnlyContext((ReadOnlyContext) result, environment, prefix + name); + } + return result; + } + + public Object lookup(Name name) throws NamingException + { + return lookup(name.toString()); + } + + public Object lookupLink(String name) throws NamingException + { + return lookup(name); + } + + public Name composeName(Name name, Name prefix) throws NamingException + { + Name result = (Name) prefix.clone(); + result.addAll(name); + return result; + } + + public String composeName(String name, String prefix) throws NamingException + { + CompositeName result = new CompositeName(prefix); + result.addAll(new CompositeName(name)); + return result.toString(); + } + + public NamingEnumeration list(String name) throws NamingException + { + Object o = lookup(name); + if (o == this) + { + return new ListEnumeration(); + } + else if (o instanceof Context) + { + return ((Context) o).list(""); + } + else + { + throw new NotContextException(); + } + } + + public NamingEnumeration listBindings(String name) throws NamingException + { + Object o = lookup(name); + if (o == this) + { + return new ListBindingEnumeration(); + } + else if (o instanceof Context) + { + return ((Context) o).listBindings(""); + } + else + { + throw new NotContextException(); + } + } + + public Object lookupLink(Name name) throws NamingException + { + return lookupLink(name.toString()); + } + + public NamingEnumeration list(Name name) throws NamingException + { + return list(name.toString()); + } + + public NamingEnumeration listBindings(Name name) throws NamingException + { + return listBindings(name.toString()); + } + + public void bind(Name name, Object obj) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void bind(String name, Object obj) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void close() throws NamingException + { + // ignore + } + + public Context createSubcontext(Name name) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public Context createSubcontext(String name) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void destroySubcontext(Name name) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void destroySubcontext(String name) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public String getNameInNamespace() throws NamingException + { + return nameInNamespace; + } + + public NameParser getNameParser(Name name) throws NamingException + { + return nameParser; + } + + public NameParser getNameParser(String name) throws NamingException + { + return nameParser; + } + + public void rebind(Name name, Object obj) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void rebind(String name, Object obj) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void rename(Name oldName, Name newName) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void rename(String oldName, String newName) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void unbind(Name name) throws NamingException + { + throw new OperationNotSupportedException(); + } + + public void unbind(String name) throws NamingException + { + throw new OperationNotSupportedException(); + } + + private abstract class LocalNamingEnumeration implements NamingEnumeration + { + private Iterator i = bindings.entrySet().iterator(); + + public boolean hasMore() throws NamingException + { + return i.hasNext(); + } + + public boolean hasMoreElements() + { + return i.hasNext(); + } + + protected Map.Entry getNext() + { + return (Map.Entry) i.next(); + } + + public void close() throws NamingException + { + } + } + + private class ListEnumeration extends LocalNamingEnumeration + { + public Object next() throws NamingException + { + return nextElement(); + } + + public Object nextElement() + { + Map.Entry entry = getNext(); + return new NameClassPair((String) entry.getKey(), entry.getValue().getClass().getName()); + } + } + + private class ListBindingEnumeration extends LocalNamingEnumeration + { + public Object next() throws NamingException + { + return nextElement(); + } + + public Object nextElement() + { + Map.Entry entry = getNext(); + return new Binding((String) entry.getKey(), entry.getValue()); + } + } +} -- cgit v1.2.1