You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by an...@apache.org on 2016/07/20 09:35:33 UTC

[1/9] activemq-artemis git commit: This closes #642 Flow control improvements

Repository: activemq-artemis
Updated Branches:
  refs/heads/master fe27cd829 -> 413e7aee5


This closes #642 Flow control improvements


Project: http://git-wip-us.apache.org/repos/asf/activemq-artemis/repo
Commit: http://git-wip-us.apache.org/repos/asf/activemq-artemis/commit/413e7aee
Tree: http://git-wip-us.apache.org/repos/asf/activemq-artemis/tree/413e7aee
Diff: http://git-wip-us.apache.org/repos/asf/activemq-artemis/diff/413e7aee

Branch: refs/heads/master
Commit: 413e7aee546dd5669a08bb90ac86ecc356c69c25
Parents: fe27cd8 4d60ced
Author: Andy Taylor <an...@gmail.com>
Authored: Wed Jul 20 10:33:44 2016 +0100
Committer: Andy Taylor <an...@gmail.com>
Committed: Wed Jul 20 10:33:44 2016 +0100

----------------------------------------------------------------------
 .../plug/ProtonSessionIntegrationCallback.java  |  64 +-
 .../org/proton/plug/AMQPSessionCallback.java    |   2 +
 .../plug/context/AbstractConnectionContext.java |   2 +-
 .../context/AbstractProtonReceiverContext.java  |   5 +-
 .../client/ProtonClientReceiverContext.java     |   5 +
 .../server/ProtonServerConnectionContext.java   |   1 -
 .../server/ProtonServerReceiverContext.java     |  21 +-
 .../server/ProtonServerSenderContext.java       |   4 +-
 .../test/minimalserver/MinimalSessionSPI.java   |   7 +-
 docs/user-manual/en/flow-control.md             |  48 +-
 tests/artemis-test-support/pom.xml              |  57 ++
 .../transport/amqp/AmqpProtocolException.java   |  62 ++
 .../activemq/transport/amqp/AmqpSupport.java    | 206 ++++
 .../amqp/client/AmqpAbstractResource.java       | 321 +++++++
 .../transport/amqp/client/AmqpClient.java       | 245 +++++
 .../transport/amqp/client/AmqpConnection.java   | 720 ++++++++++++++
 .../amqp/client/AmqpConnectionListener.java     |  31 +
 .../client/AmqpDefaultConnectionListener.java   |  28 +
 .../transport/amqp/client/AmqpEventSink.java    |  69 ++
 .../amqp/client/AmqpJmsSelectorFilter.java      |  48 +
 .../transport/amqp/client/AmqpMessage.java      | 515 ++++++++++
 .../amqp/client/AmqpNoLocalFilter.java          |  45 +
 .../transport/amqp/client/AmqpReceiver.java     | 946 +++++++++++++++++++
 .../amqp/client/AmqpRedirectedException.java    |  61 ++
 .../transport/amqp/client/AmqpResource.java     | 108 +++
 .../transport/amqp/client/AmqpSender.java       | 452 +++++++++
 .../transport/amqp/client/AmqpSession.java      | 454 +++++++++
 .../transport/amqp/client/AmqpSupport.java      | 195 ++++
 .../amqp/client/AmqpTransactionContext.java     | 261 +++++
 .../amqp/client/AmqpTransactionCoordinator.java | 262 +++++
 .../amqp/client/AmqpTransactionId.java          |  98 ++
 .../amqp/client/AmqpTransferTagGenerator.java   | 104 ++
 .../amqp/client/AmqpUnknownFilterType.java      |  49 +
 .../transport/amqp/client/AmqpValidator.java    | 101 ++
 .../amqp/client/sasl/AbstractMechanism.java     |  97 ++
 .../amqp/client/sasl/AnonymousMechanism.java    |  43 +
 .../amqp/client/sasl/CramMD5Mechanism.java      |  94 ++
 .../transport/amqp/client/sasl/Mechanism.java   | 143 +++
 .../amqp/client/sasl/PlainMechanism.java        |  76 ++
 .../amqp/client/sasl/SaslAuthenticator.java     | 182 ++++
 .../client/transport/NettyTcpTransport.java     | 402 ++++++++
 .../amqp/client/transport/NettyTransport.java   |  52 +
 .../client/transport/NettyTransportFactory.java |  80 ++
 .../transport/NettyTransportListener.java       |  46 +
 .../client/transport/NettyTransportOptions.java | 177 ++++
 .../transport/NettyTransportSslOptions.java     | 284 ++++++
 .../client/transport/NettyTransportSupport.java | 288 ++++++
 .../amqp/client/transport/NettyWSTransport.java | 472 +++++++++
 .../PartialPooledByteBufAllocator.java          | 134 +++
 .../client/transport/X509AliasKeyManager.java   |  86 ++
 .../transport/amqp/client/util/AsyncResult.java |  46 +
 .../amqp/client/util/ClientFuture.java          | 110 +++
 .../util/ClientFutureSynchronization.java       |  30 +
 .../amqp/client/util/IOExceptionSupport.java    |  45 +
 .../transport/amqp/client/util/IdGenerator.java | 274 ++++++
 .../amqp/client/util/NoOpAsyncResult.java       |  40 +
 .../amqp/client/util/PropertyUtil.java          | 533 +++++++++++
 .../amqp/client/util/StringArrayConverter.java  |  64 ++
 .../amqp/client/util/TypeConversionSupport.java | 218 +++++
 .../client/util/UnmodifiableConnection.java     | 202 ++++
 .../amqp/client/util/UnmodifiableDelivery.java  | 170 ++++
 .../amqp/client/util/UnmodifiableLink.java      | 276 ++++++
 .../amqp/client/util/UnmodifiableReceiver.java  |  59 ++
 .../amqp/client/util/UnmodifiableSender.java    |  45 +
 .../amqp/client/util/UnmodifiableSession.java   | 150 +++
 .../amqp/client/util/UnmodifiableTransport.java | 274 ++++++
 .../amqp/client/util/WrappedAsyncResult.java    |  59 ++
 tests/integration-tests/pom.xml                 |   5 +
 .../tests/integration/proton/ProtonTest.java    | 207 +++-
 tests/pom.xml                                   |   8 +
 70 files changed, 11029 insertions(+), 39 deletions(-)
----------------------------------------------------------------------



[7/9] activemq-artemis git commit: ARTEMIS-637 Port 5.x AMQP test client

Posted by an...@apache.org.
http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSession.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSession.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSession.java
new file mode 100644
index 0000000..28e38f2
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSession.java
@@ -0,0 +1,454 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.activemq.transport.amqp.client.util.ClientFuture;
+import org.apache.activemq.transport.amqp.client.util.UnmodifiableSession;
+import org.apache.qpid.proton.amqp.messaging.Source;
+import org.apache.qpid.proton.amqp.messaging.Target;
+import org.apache.qpid.proton.engine.Connection;
+import org.apache.qpid.proton.engine.Session;
+
+/**
+ * Session class that manages a Proton session endpoint.
+ */
+public class AmqpSession extends AmqpAbstractResource<Session> {
+
+   private final AtomicLong receiverIdGenerator = new AtomicLong();
+   private final AtomicLong senderIdGenerator = new AtomicLong();
+
+   private final AmqpConnection connection;
+   private final String sessionId;
+   private final AmqpTransactionContext txContext;
+
+   /**
+    * Create a new session instance.
+    *
+    * @param connection The parent connection that created the session.
+    * @param sessionId  The unique ID value assigned to this session.
+    */
+   public AmqpSession(AmqpConnection connection, String sessionId) {
+      this.connection = connection;
+      this.sessionId = sessionId;
+      this.txContext = new AmqpTransactionContext(this);
+   }
+
+   /**
+    * Create a sender instance using the given address
+    *
+    * @param address the address to which the sender will produce its messages.
+    * @return a newly created sender that is ready for use.
+    * @throws Exception if an error occurs while creating the sender.
+    */
+   public AmqpSender createSender(final String address) throws Exception {
+      return createSender(address, false);
+   }
+
+   /**
+    * Create a sender instance using the given address
+    *
+    * @param address   the address to which the sender will produce its messages.
+    * @param presettle controls if the created sender produces message that have already been marked settled.
+    * @return a newly created sender that is ready for use.
+    * @throws Exception if an error occurs while creating the sender.
+    */
+   public AmqpSender createSender(final String address, boolean presettle) throws Exception {
+      checkClosed();
+
+      final AmqpSender sender = new AmqpSender(AmqpSession.this, address, getNextSenderId());
+      sender.setPresettle(presettle);
+      final ClientFuture request = new ClientFuture();
+
+      connection.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            sender.setStateInspector(getStateInspector());
+            sender.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return sender;
+   }
+
+   /**
+    * Create a sender instance using the given Target
+    *
+    * @param target the caller created and configured Traget used to create the sender link.
+    * @return a newly created sender that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpSender createSender(Target target) throws Exception {
+      checkClosed();
+
+      final AmqpSender sender = new AmqpSender(AmqpSession.this, target, getNextSenderId());
+      final ClientFuture request = new ClientFuture();
+
+      connection.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            sender.setStateInspector(getStateInspector());
+            sender.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return sender;
+   }
+
+   /**
+    * Create a receiver instance using the given address
+    *
+    * @param address the address to which the receiver will subscribe for its messages.
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createReceiver(String address) throws Exception {
+      return createReceiver(address, null, false);
+   }
+
+   /**
+    * Create a receiver instance using the given address
+    *
+    * @param address  the address to which the receiver will subscribe for its messages.
+    * @param selector the JMS selector to use for the subscription
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createReceiver(String address, String selector) throws Exception {
+      return createReceiver(address, selector, false);
+   }
+
+   /**
+    * Create a receiver instance using the given address
+    *
+    * @param address  the address to which the receiver will subscribe for its messages.
+    * @param selector the JMS selector to use for the subscription
+    * @param noLocal  should the subscription have messages from its connection filtered.
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createReceiver(String address, String selector, boolean noLocal) throws Exception {
+      return createReceiver(address, selector, noLocal, false);
+   }
+
+   /**
+    * Create a receiver instance using the given address
+    *
+    * @param address   the address to which the receiver will subscribe for its messages.
+    * @param selector  the JMS selector to use for the subscription
+    * @param noLocal   should the subscription have messages from its connection filtered.
+    * @param presettle should the receiver be created with a settled sender mode.
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createReceiver(String address,
+                                      String selector,
+                                      boolean noLocal,
+                                      boolean presettle) throws Exception {
+      checkClosed();
+
+      final ClientFuture request = new ClientFuture();
+      final AmqpReceiver receiver = new AmqpReceiver(AmqpSession.this, address, getNextReceiverId());
+
+      receiver.setNoLocal(noLocal);
+      receiver.setPresettle(presettle);
+      if (selector != null && !selector.isEmpty()) {
+         receiver.setSelector(selector);
+      }
+
+      connection.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            receiver.setStateInspector(getStateInspector());
+            receiver.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return receiver;
+   }
+
+   /**
+    * Create a receiver instance using the given Source
+    *
+    * @param source the caller created and configured Source used to create the receiver link.
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createReceiver(Source source) throws Exception {
+      checkClosed();
+
+      final ClientFuture request = new ClientFuture();
+      final AmqpReceiver receiver = new AmqpReceiver(AmqpSession.this, source, getNextReceiverId());
+
+      connection.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            receiver.setStateInspector(getStateInspector());
+            receiver.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return receiver;
+   }
+
+   /**
+    * Create a receiver instance using the given address that creates a durable subscription.
+    *
+    * @param address          the address to which the receiver will subscribe for its messages.
+    * @param subscriptionName the name of the subscription that is being created.
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createDurableReceiver(String address, String subscriptionName) throws Exception {
+      return createDurableReceiver(address, subscriptionName, null, false);
+   }
+
+   /**
+    * Create a receiver instance using the given address that creates a durable subscription.
+    *
+    * @param address          the address to which the receiver will subscribe for its messages.
+    * @param subscriptionName the name of the subscription that is being created.
+    * @param selector         the JMS selector to use for the subscription
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createDurableReceiver(String address,
+                                             String subscriptionName,
+                                             String selector) throws Exception {
+      return createDurableReceiver(address, subscriptionName, selector, false);
+   }
+
+   /**
+    * Create a receiver instance using the given address that creates a durable subscription.
+    *
+    * @param address          the address to which the receiver will subscribe for its messages.
+    * @param subscriptionName the name of the subscription that is being created.
+    * @param selector         the JMS selector to use for the subscription
+    * @param noLocal          should the subscription have messages from its connection filtered.
+    * @return a newly created receiver that is ready for use.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver createDurableReceiver(String address,
+                                             String subscriptionName,
+                                             String selector,
+                                             boolean noLocal) throws Exception {
+      checkClosed();
+
+      if (subscriptionName == null || subscriptionName.isEmpty()) {
+         throw new IllegalArgumentException("subscription name must not be null or empty.");
+      }
+
+      final ClientFuture request = new ClientFuture();
+      final AmqpReceiver receiver = new AmqpReceiver(AmqpSession.this, address, getNextReceiverId());
+      receiver.setSubscriptionName(subscriptionName);
+      receiver.setNoLocal(noLocal);
+      if (selector != null && !selector.isEmpty()) {
+         receiver.setSelector(selector);
+      }
+
+      connection.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            receiver.setStateInspector(getStateInspector());
+            receiver.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return receiver;
+   }
+
+   /**
+    * Create a receiver instance using the given address that creates a durable subscription.
+    *
+    * @param subscriptionName the name of the subscription that should be queried for on the remote..
+    * @return a newly created receiver that is ready for use if the subscription exists.
+    * @throws Exception if an error occurs while creating the receiver.
+    */
+   public AmqpReceiver lookupSubscription(String subscriptionName) throws Exception {
+      checkClosed();
+
+      if (subscriptionName == null || subscriptionName.isEmpty()) {
+         throw new IllegalArgumentException("subscription name must not be null or empty.");
+      }
+
+      final ClientFuture request = new ClientFuture();
+      final AmqpReceiver receiver = new AmqpReceiver(AmqpSession.this, (String) null, getNextReceiverId());
+      receiver.setSubscriptionName(subscriptionName);
+
+      connection.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            receiver.setStateInspector(getStateInspector());
+            receiver.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return receiver;
+   }
+
+   /**
+    * @return this session's parent AmqpConnection.
+    */
+   public AmqpConnection getConnection() {
+      return connection;
+   }
+
+   public Session getSession() {
+      return new UnmodifiableSession(getEndpoint());
+   }
+
+   public boolean isInTransaction() {
+      return txContext.isInTransaction();
+   }
+
+   @Override
+   public String toString() {
+      return "AmqpSession { " + sessionId + " }";
+   }
+
+   //----- Session Transaction Methods --------------------------------------//
+
+   /**
+    * Starts a new transaction associated with this session.
+    *
+    * @throws Exception if an error occurs starting a new Transaction.
+    */
+   public void begin() throws Exception {
+      if (txContext.isInTransaction()) {
+         throw new javax.jms.IllegalStateException("Session already has an active transaction");
+      }
+
+      txContext.begin();
+   }
+
+   /**
+    * Commit the current transaction associated with this session.
+    *
+    * @throws Exception if an error occurs committing the Transaction.
+    */
+   public void commit() throws Exception {
+      if (!txContext.isInTransaction()) {
+         throw new javax.jms.IllegalStateException("Commit called on Session that does not have an active transaction");
+      }
+
+      txContext.commit();
+   }
+
+   /**
+    * Roll back the current transaction associated with this session.
+    *
+    * @throws Exception if an error occurs rolling back the Transaction.
+    */
+   public void rollback() throws Exception {
+      if (!txContext.isInTransaction()) {
+         throw new javax.jms.IllegalStateException("Rollback called on Session that does not have an active transaction");
+      }
+
+      txContext.rollback();
+   }
+
+   //----- Internal access used to manage resources -------------------------//
+
+   ScheduledExecutorService getScheduler() {
+      return connection.getScheduler();
+   }
+
+   Connection getProtonConnection() {
+      return connection.getProtonConnection();
+   }
+
+   void pumpToProtonTransport(AsyncResult request) {
+      connection.pumpToProtonTransport(request);
+   }
+
+   AmqpTransactionId getTransactionId() {
+      return txContext.getTransactionId();
+   }
+
+   AmqpTransactionContext getTransactionContext() {
+      return txContext;
+   }
+
+   //----- Private implementation details -----------------------------------//
+
+   @Override
+   protected void doOpenInspection() {
+      try {
+         getStateInspector().inspectOpenedResource(getSession());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected void doClosedInspection() {
+      try {
+         getStateInspector().inspectClosedResource(getSession());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   private String getNextSenderId() {
+      return sessionId + ":" + senderIdGenerator.incrementAndGet();
+   }
+
+   private String getNextReceiverId() {
+      return sessionId + ":" + receiverIdGenerator.incrementAndGet();
+   }
+
+   private void checkClosed() {
+      if (isClosed() || connection.isClosed()) {
+         throw new IllegalStateException("Session is already closed");
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSupport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSupport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSupport.java
new file mode 100644
index 0000000..c9ee57b
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSupport.java
@@ -0,0 +1,195 @@
+/*
+ * 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.activemq.transport.amqp.client;
+
+import javax.jms.InvalidClientIDException;
+import javax.jms.InvalidDestinationException;
+import javax.jms.JMSException;
+import javax.jms.JMSSecurityException;
+import javax.jms.ResourceAllocationException;
+import javax.jms.TransactionRolledBackException;
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.Modified;
+import org.apache.qpid.proton.amqp.messaging.Rejected;
+import org.apache.qpid.proton.amqp.transaction.TransactionErrors;
+import org.apache.qpid.proton.amqp.transport.AmqpError;
+import org.apache.qpid.proton.amqp.transport.ConnectionError;
+import org.apache.qpid.proton.amqp.transport.ErrorCondition;
+
+public class AmqpSupport {
+
+   // Symbols used for connection capabilities
+   public static final Symbol SOLE_CONNECTION_CAPABILITY = Symbol.valueOf("sole-connection-for-container");
+   public static final Symbol ANONYMOUS_RELAY = Symbol.valueOf("ANONYMOUS-RELAY");
+
+   // Symbols used to announce connection error information
+   public static final Symbol CONNECTION_OPEN_FAILED = Symbol.valueOf("amqp:connection-establishment-failed");
+   public static final Symbol INVALID_FIELD = Symbol.valueOf("invalid-field");
+   public static final Symbol CONTAINER_ID = Symbol.valueOf("container-id");
+
+   // Symbols used to announce connection redirect ErrorCondition 'info'
+   public static final Symbol PORT = Symbol.valueOf("port");
+   public static final Symbol NETWORK_HOST = Symbol.valueOf("network-host");
+   public static final Symbol OPEN_HOSTNAME = Symbol.valueOf("hostname");
+
+   // Symbols used for connection properties
+   public static final Symbol QUEUE_PREFIX = Symbol.valueOf("queue-prefix");
+   public static final Symbol TOPIC_PREFIX = Symbol.valueOf("topic-prefix");
+
+   public static final Symbol PRODUCT = Symbol.valueOf("product");
+   public static final Symbol VERSION = Symbol.valueOf("version");
+   public static final Symbol PLATFORM = Symbol.valueOf("platform");
+
+   // Symbols used for receivers.
+   public static final Symbol COPY = Symbol.getSymbol("copy");
+   public static final Symbol NO_LOCAL_SYMBOL = Symbol.valueOf("no-local");
+   public static final Symbol SELECTOR_SYMBOL = Symbol.valueOf("jms-selector");
+
+   // Delivery states
+   public static final Rejected REJECTED = new Rejected();
+   public static final Modified MODIFIED_FAILED = new Modified();
+   public static final Modified MODIFIED_FAILED_UNDELIVERABLE = new Modified();
+
+   // Temporary Destination constants
+   public static final Symbol DYNAMIC_NODE_LIFETIME_POLICY = Symbol.valueOf("lifetime-policy");
+   public static final String TEMP_QUEUE_CREATOR = "temp-queue-creator:";
+   public static final String TEMP_TOPIC_CREATOR = "temp-topic-creator:";
+
+   //----- Static initializer -----------------------------------------------//
+
+   static {
+      MODIFIED_FAILED.setDeliveryFailed(true);
+
+      MODIFIED_FAILED_UNDELIVERABLE.setDeliveryFailed(true);
+      MODIFIED_FAILED_UNDELIVERABLE.setUndeliverableHere(true);
+   }
+
+   //----- Utility Methods --------------------------------------------------//
+
+   /**
+    * Given an ErrorCondition instance create a new Exception that best matches
+    * the error type.
+    *
+    * @param errorCondition The ErrorCondition returned from the remote peer.
+    * @return a new Exception instance that best matches the ErrorCondition value.
+    */
+   public static Exception convertToException(ErrorCondition errorCondition) {
+      Exception remoteError = null;
+
+      if (errorCondition != null && errorCondition.getCondition() != null) {
+         Symbol error = errorCondition.getCondition();
+         String message = extractErrorMessage(errorCondition);
+
+         if (error.equals(AmqpError.UNAUTHORIZED_ACCESS)) {
+            remoteError = new JMSSecurityException(message);
+         }
+         else if (error.equals(AmqpError.RESOURCE_LIMIT_EXCEEDED)) {
+            remoteError = new ResourceAllocationException(message);
+         }
+         else if (error.equals(AmqpError.NOT_FOUND)) {
+            remoteError = new InvalidDestinationException(message);
+         }
+         else if (error.equals(TransactionErrors.TRANSACTION_ROLLBACK)) {
+            remoteError = new TransactionRolledBackException(message);
+         }
+         else if (error.equals(ConnectionError.REDIRECT)) {
+            remoteError = createRedirectException(error, message, errorCondition);
+         }
+         else if (error.equals(AmqpError.INVALID_FIELD)) {
+            Map<?, ?> info = errorCondition.getInfo();
+            if (info != null && CONTAINER_ID.equals(info.get(INVALID_FIELD))) {
+               remoteError = new InvalidClientIDException(message);
+            }
+            else {
+               remoteError = new JMSException(message);
+            }
+         }
+         else {
+            remoteError = new JMSException(message);
+         }
+      }
+      else {
+         remoteError = new JMSException("Unknown error from remote peer");
+      }
+
+      return remoteError;
+   }
+
+   /**
+    * Attempt to read and return the embedded error message in the given ErrorCondition
+    * object.  If no message can be extracted a generic message is returned.
+    *
+    * @param errorCondition The ErrorCondition to extract the error message from.
+    * @return an error message extracted from the given ErrorCondition.
+    */
+   public static String extractErrorMessage(ErrorCondition errorCondition) {
+      String message = "Received error from remote peer without description";
+      if (errorCondition != null) {
+         if (errorCondition.getDescription() != null && !errorCondition.getDescription().isEmpty()) {
+            message = errorCondition.getDescription();
+         }
+
+         Symbol condition = errorCondition.getCondition();
+         if (condition != null) {
+            message = message + " [condition = " + condition + "]";
+         }
+      }
+
+      return message;
+   }
+
+   /**
+    * When a redirect type exception is received this method is called to create the
+    * appropriate redirect exception type containing the error details needed.
+    *
+    * @param error     the Symbol that defines the redirection error type.
+    * @param message   the basic error message that should used or amended for the returned exception.
+    * @param condition the ErrorCondition that describes the redirection.
+    * @return an Exception that captures the details of the redirection error.
+    */
+   public static Exception createRedirectException(Symbol error, String message, ErrorCondition condition) {
+      Exception result = null;
+      Map<?, ?> info = condition.getInfo();
+
+      if (info == null) {
+         result = new IOException(message + " : Redirection information not set.");
+      }
+      else {
+         String hostname = (String) info.get(OPEN_HOSTNAME);
+
+         String networkHost = (String) info.get(NETWORK_HOST);
+         if (networkHost == null || networkHost.isEmpty()) {
+            result = new IOException(message + " : Redirection information not set.");
+         }
+
+         int port = 0;
+         try {
+            port = Integer.valueOf(info.get(PORT).toString());
+         }
+         catch (Exception ex) {
+            result = new IOException(message + " : Redirection information not set.");
+         }
+
+         result = new AmqpRedirectedException(message, hostname, networkHost, port);
+      }
+
+      return result;
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionContext.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionContext.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionContext.java
new file mode 100644
index 0000000..dcf23d2
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionContext.java
@@ -0,0 +1,261 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.activemq.transport.amqp.client.util.ClientFuture;
+import org.apache.activemq.transport.amqp.client.util.ClientFutureSynchronization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Defines a context under which resources in a given session
+ * will operate inside transaction scoped boundaries.
+ */
+public class AmqpTransactionContext {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpTransactionContext.class);
+
+   private final AmqpSession session;
+   private final Set<AmqpReceiver> txReceivers = new LinkedHashSet<>();
+
+   private AmqpTransactionCoordinator coordinator;
+   private AmqpTransactionId transactionId;
+
+   public AmqpTransactionContext(AmqpSession session) {
+      this.session = session;
+   }
+
+   /**
+    * Begins a new transaction scoped to the target session.
+    *
+    * @param txId The transaction Id to use for this new transaction.
+    * @throws Exception if an error occurs while starting the transaction.
+    */
+   public void begin() throws Exception {
+      if (transactionId != null) {
+         throw new IOException("Begin called while a TX is still Active.");
+      }
+
+      final AmqpTransactionId txId = session.getConnection().getNextTransactionId();
+      final ClientFuture request = new ClientFuture(new ClientFutureSynchronization() {
+
+         @Override
+         public void onPendingSuccess() {
+            transactionId = txId;
+         }
+
+         @Override
+         public void onPendingFailure(Throwable cause) {
+            transactionId = null;
+         }
+      });
+
+      LOG.info("Attempting to Begin TX:[{}]", txId);
+
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            if (coordinator == null || coordinator.isClosed()) {
+               LOG.info("Creating new Coordinator for TX:[{}]", txId);
+               coordinator = new AmqpTransactionCoordinator(session);
+               coordinator.open(new AsyncResult() {
+
+                  @Override
+                  public void onSuccess() {
+                     try {
+                        LOG.info("Attempting to declare TX:[{}]", txId);
+                        coordinator.declare(txId, request);
+                     }
+                     catch (Exception e) {
+                        request.onFailure(e);
+                     }
+                  }
+
+                  @Override
+                  public void onFailure(Throwable result) {
+                     request.onFailure(result);
+                  }
+
+                  @Override
+                  public boolean isComplete() {
+                     return request.isComplete();
+                  }
+               });
+            }
+            else {
+               try {
+                  LOG.info("Attempting to declare TX:[{}]", txId);
+                  coordinator.declare(txId, request);
+               }
+               catch (Exception e) {
+                  request.onFailure(e);
+               }
+            }
+
+            session.pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Commit this transaction which then ends the lifetime of the transacted operation.
+    *
+    * @throws Exception if an error occurs while performing the commit
+    */
+   public void commit() throws Exception {
+      if (transactionId == null) {
+         throw new IllegalStateException("Commit called with no active Transaction.");
+      }
+
+      preCommit();
+
+      final ClientFuture request = new ClientFuture(new ClientFutureSynchronization() {
+
+         @Override
+         public void onPendingSuccess() {
+            transactionId = null;
+            postCommit();
+         }
+
+         @Override
+         public void onPendingFailure(Throwable cause) {
+            transactionId = null;
+            postCommit();
+         }
+      });
+
+      LOG.debug("Commit on TX[{}] initiated", transactionId);
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            try {
+               LOG.info("Attempting to commit TX:[{}]", transactionId);
+               coordinator.discharge(transactionId, request, true);
+               session.pumpToProtonTransport(request);
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Rollback any transacted work performed under the current transaction.
+    *
+    * @throws Exception if an error occurs during the rollback operation.
+    */
+   public void rollback() throws Exception {
+      if (transactionId == null) {
+         throw new IllegalStateException("Rollback called with no active Transaction.");
+      }
+
+      preRollback();
+
+      final ClientFuture request = new ClientFuture(new ClientFutureSynchronization() {
+
+         @Override
+         public void onPendingSuccess() {
+            transactionId = null;
+            postRollback();
+         }
+
+         @Override
+         public void onPendingFailure(Throwable cause) {
+            transactionId = null;
+            postRollback();
+         }
+      });
+
+      LOG.debug("Rollback on TX[{}] initiated", transactionId);
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            try {
+               LOG.info("Attempting to roll back TX:[{}]", transactionId);
+               coordinator.discharge(transactionId, request, false);
+               session.pumpToProtonTransport(request);
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   //----- Internal access to context properties ----------------------------//
+
+   AmqpTransactionCoordinator getCoordinator() {
+      return coordinator;
+   }
+
+   AmqpTransactionId getTransactionId() {
+      return transactionId;
+   }
+
+   boolean isInTransaction() {
+      return transactionId != null;
+   }
+
+   void registerTxConsumer(AmqpReceiver consumer) {
+      txReceivers.add(consumer);
+   }
+
+   //----- Transaction pre / post completion --------------------------------//
+
+   private void preCommit() {
+      for (AmqpReceiver receiver : txReceivers) {
+         receiver.preCommit();
+      }
+   }
+
+   private void preRollback() {
+      for (AmqpReceiver receiver : txReceivers) {
+         receiver.preRollback();
+      }
+   }
+
+   private void postCommit() {
+      for (AmqpReceiver receiver : txReceivers) {
+         receiver.postCommit();
+      }
+
+      txReceivers.clear();
+   }
+
+   private void postRollback() {
+      for (AmqpReceiver receiver : txReceivers) {
+         receiver.postRollback();
+      }
+
+      txReceivers.clear();
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionCoordinator.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionCoordinator.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionCoordinator.java
new file mode 100644
index 0000000..aded162
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionCoordinator.java
@@ -0,0 +1,262 @@
+/*
+ * 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.activemq.transport.amqp.client;
+
+import javax.jms.IllegalStateException;
+import javax.jms.JMSException;
+import javax.jms.TransactionRolledBackException;
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.activemq.transport.amqp.client.util.IOExceptionSupport;
+import org.apache.qpid.proton.amqp.messaging.AmqpValue;
+import org.apache.qpid.proton.amqp.messaging.Rejected;
+import org.apache.qpid.proton.amqp.messaging.Source;
+import org.apache.qpid.proton.amqp.transaction.Coordinator;
+import org.apache.qpid.proton.amqp.transaction.Declare;
+import org.apache.qpid.proton.amqp.transaction.Declared;
+import org.apache.qpid.proton.amqp.transaction.Discharge;
+import org.apache.qpid.proton.amqp.transaction.TxnCapability;
+import org.apache.qpid.proton.amqp.transport.DeliveryState;
+import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
+import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.engine.Sender;
+import org.apache.qpid.proton.message.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents the AMQP Transaction coordinator link used by the transaction context
+ * of a session to control the lifetime of a given transaction.
+ */
+public class AmqpTransactionCoordinator extends AmqpAbstractResource<Sender> {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpTransactionCoordinator.class);
+
+   private final byte[] OUTBOUND_BUFFER = new byte[64];
+
+   private final AmqpSession session;
+   private final AmqpTransferTagGenerator tagGenerator = new AmqpTransferTagGenerator();
+
+   private List<Delivery> pendingDeliveries = new LinkedList<>();
+   private Map<AmqpTransactionId, AsyncResult> pendingRequests = new HashMap<>();
+
+   public AmqpTransactionCoordinator(AmqpSession session) {
+      this.session = session;
+   }
+
+   @Override
+   public void processDeliveryUpdates(AmqpConnection connection) throws IOException {
+      try {
+         Iterator<Delivery> deliveries = pendingDeliveries.iterator();
+         while (deliveries.hasNext()) {
+            Delivery pendingDelivery = deliveries.next();
+            if (!pendingDelivery.remotelySettled()) {
+               continue;
+            }
+
+            DeliveryState state = pendingDelivery.getRemoteState();
+            AmqpTransactionId txId = (AmqpTransactionId) pendingDelivery.getContext();
+            AsyncResult pendingRequest = pendingRequests.get(txId);
+
+            if (pendingRequest == null) {
+               throw new IllegalStateException("Pending tx operation with no pending request");
+            }
+
+            if (state instanceof Declared) {
+               LOG.debug("New TX started: {}", txId.getTxId());
+               Declared declared = (Declared) state;
+               txId.setRemoteTxId(declared.getTxnId());
+               pendingRequest.onSuccess();
+            }
+            else if (state instanceof Rejected) {
+               LOG.debug("Last TX request failed: {}", txId.getTxId());
+               Rejected rejected = (Rejected) state;
+               Exception cause = AmqpSupport.convertToException(rejected.getError());
+               JMSException failureCause = null;
+               if (txId.isCommit()) {
+                  failureCause = new TransactionRolledBackException(cause.getMessage());
+               }
+               else {
+                  failureCause = new JMSException(cause.getMessage());
+               }
+
+               pendingRequest.onFailure(failureCause);
+            }
+            else {
+               LOG.debug("Last TX request succeeded: {}", txId.getTxId());
+               pendingRequest.onSuccess();
+            }
+
+            // Clear state data
+            pendingDelivery.settle();
+            pendingRequests.remove(txId);
+            deliveries.remove();
+         }
+
+         super.processDeliveryUpdates(connection);
+      }
+      catch (Exception e) {
+         throw IOExceptionSupport.create(e);
+      }
+   }
+
+   public void declare(AmqpTransactionId txId, AsyncResult request) throws Exception {
+      if (txId.getRemoteTxId() != null) {
+         throw new IllegalStateException("Declar called while a TX is still Active.");
+      }
+
+      if (isClosed()) {
+         request.onFailure(new JMSException("Cannot start new transaction: Coordinator remotely closed"));
+         return;
+      }
+
+      Message message = Message.Factory.create();
+      Declare declare = new Declare();
+      message.setBody(new AmqpValue(declare));
+
+      Delivery pendingDelivery = getEndpoint().delivery(tagGenerator.getNextTag());
+      pendingDelivery.setContext(txId);
+
+      // Store away for completion
+      pendingDeliveries.add(pendingDelivery);
+      pendingRequests.put(txId, request);
+
+      sendTxCommand(message);
+   }
+
+   public void discharge(AmqpTransactionId txId, AsyncResult request, boolean commit) throws Exception {
+
+      if (isClosed()) {
+         Exception failureCause = null;
+
+         if (commit) {
+            failureCause = new TransactionRolledBackException("Transaction inbout: Coordinator remotely closed");
+         }
+         else {
+            failureCause = new JMSException("Rollback cannot complete: Coordinator remotely closed");
+         }
+
+         request.onFailure(failureCause);
+         return;
+      }
+
+      // Store the context of this action in the transaction ID for later completion.
+      txId.setState(commit ? AmqpTransactionId.COMMIT_MARKER : AmqpTransactionId.ROLLBACK_MARKER);
+
+      Message message = Message.Factory.create();
+      Discharge discharge = new Discharge();
+      discharge.setFail(!commit);
+      discharge.setTxnId(txId.getRemoteTxId());
+      message.setBody(new AmqpValue(discharge));
+
+      Delivery pendingDelivery = getEndpoint().delivery(tagGenerator.getNextTag());
+      pendingDelivery.setContext(txId);
+
+      // Store away for completion
+      pendingDeliveries.add(pendingDelivery);
+      pendingRequests.put(txId, request);
+
+      sendTxCommand(message);
+   }
+
+   //----- Base class overrides ---------------------------------------------//
+
+   @Override
+   public void remotelyClosed(AmqpConnection connection) {
+
+      Exception txnError = AmqpSupport.convertToException(getEndpoint().getRemoteCondition());
+
+      // Alert any pending operation that the link failed to complete the pending
+      // begin / commit / rollback operation.
+      for (AsyncResult pendingRequest : pendingRequests.values()) {
+         pendingRequest.onFailure(txnError);
+      }
+
+      // Purge linkages to pending operations.
+      pendingDeliveries.clear();
+      pendingRequests.clear();
+
+      // Override the base class version because we do not want to propagate
+      // an error up to the client if remote close happens as that is an
+      // acceptable way for the remote to indicate the discharge could not
+      // be applied.
+
+      if (getEndpoint() != null) {
+         getEndpoint().close();
+         getEndpoint().free();
+      }
+
+      LOG.debug("Transaction Coordinator link {} was remotely closed", getEndpoint());
+   }
+
+   //----- Internal implementation ------------------------------------------//
+
+   private void sendTxCommand(Message message) throws IOException {
+      int encodedSize = 0;
+      byte[] buffer = OUTBOUND_BUFFER;
+      while (true) {
+         try {
+            encodedSize = message.encode(buffer, 0, buffer.length);
+            break;
+         }
+         catch (BufferOverflowException e) {
+            buffer = new byte[buffer.length * 2];
+         }
+      }
+
+      Sender sender = getEndpoint();
+      sender.send(buffer, 0, encodedSize);
+      sender.advance();
+   }
+
+   @Override
+   protected void doOpen() {
+      Coordinator coordinator = new Coordinator();
+      coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
+      Source source = new Source();
+
+      String coordinatorName = "qpid-jms:coordinator:" + session.getConnection().getConnectionId();
+
+      Sender sender = session.getEndpoint().sender(coordinatorName);
+      sender.setSource(source);
+      sender.setTarget(coordinator);
+      sender.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+      sender.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+
+      setEndpoint(sender);
+
+      super.doOpen();
+   }
+
+   @Override
+   protected void doOpenInspection() {
+      // TODO
+   }
+
+   @Override
+   protected void doClosedInspection() {
+      // TODO
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionId.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionId.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionId.java
new file mode 100644
index 0000000..5dcdfe1
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransactionId.java
@@ -0,0 +1,98 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import org.apache.qpid.proton.amqp.Binary;
+
+/**
+ * Wrapper For Transaction state in identification
+ */
+public class AmqpTransactionId {
+
+   public static final int DECLARE_MARKER = 1;
+   public static final int ROLLBACK_MARKER = 2;
+   public static final int COMMIT_MARKER = 3;
+
+   private final String txId;
+   private Binary remoteTxId;
+   private int state = DECLARE_MARKER;
+
+   public AmqpTransactionId(String txId) {
+      this.txId = txId;
+   }
+
+   public boolean isDeclare() {
+      return state == DECLARE_MARKER;
+   }
+
+   public boolean isCommit() {
+      return state == COMMIT_MARKER;
+   }
+
+   public boolean isRollback() {
+      return state == ROLLBACK_MARKER;
+   }
+
+   public void setState(int state) {
+      this.state = state;
+   }
+
+   public String getTxId() {
+      return txId;
+   }
+
+   public Binary getRemoteTxId() {
+      return remoteTxId;
+   }
+
+   public void setRemoteTxId(Binary remoteTxId) {
+      this.remoteTxId = remoteTxId;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((txId == null) ? 0 : txId.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+
+      AmqpTransactionId other = (AmqpTransactionId) obj;
+      if (txId == null) {
+         if (other.txId != null) {
+            return false;
+         }
+      }
+      else if (!txId.equals(other.txId)) {
+         return false;
+      }
+
+      return true;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransferTagGenerator.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransferTagGenerator.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransferTagGenerator.java
new file mode 100644
index 0000000..85ee07f
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpTransferTagGenerator.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.activemq.transport.amqp.client;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Utility class that can generate and if enabled pool the binary tag values
+ * used to identify transfers over an AMQP link.
+ */
+public final class AmqpTransferTagGenerator {
+
+   public static final int DEFAULT_TAG_POOL_SIZE = 1024;
+
+   private long nextTagId;
+   private int maxPoolSize = DEFAULT_TAG_POOL_SIZE;
+
+   private final Set<byte[]> tagPool;
+
+   public AmqpTransferTagGenerator() {
+      this(false);
+   }
+
+   public AmqpTransferTagGenerator(boolean pool) {
+      if (pool) {
+         this.tagPool = new LinkedHashSet<>();
+      }
+      else {
+         this.tagPool = null;
+      }
+   }
+
+   /**
+    * Retrieves the next available tag.
+    *
+    * @return a new or unused tag depending on the pool option.
+    */
+   public byte[] getNextTag() {
+      byte[] rc;
+      if (tagPool != null && !tagPool.isEmpty()) {
+         final Iterator<byte[]> iterator = tagPool.iterator();
+         rc = iterator.next();
+         iterator.remove();
+      }
+      else {
+         try {
+            rc = Long.toHexString(nextTagId++).getBytes("UTF-8");
+         }
+         catch (UnsupportedEncodingException e) {
+            // This should never happen since we control the input.
+            throw new RuntimeException(e);
+         }
+      }
+      return rc;
+   }
+
+   /**
+    * When used as a pooled cache of tags the unused tags should always be returned once
+    * the transfer has been settled.
+    *
+    * @param data a previously borrowed tag that is no longer in use.
+    */
+   public void returnTag(byte[] data) {
+      if (tagPool != null && tagPool.size() < maxPoolSize) {
+         tagPool.add(data);
+      }
+   }
+
+   /**
+    * Gets the current max pool size value.
+    *
+    * @return the current max tag pool size.
+    */
+   public int getMaxPoolSize() {
+      return maxPoolSize;
+   }
+
+   /**
+    * Sets the max tag pool size.  If the size is smaller than the current number
+    * of pooled tags the pool will drain over time until it matches the max.
+    *
+    * @param maxPoolSize the maximum number of tags to hold in the pool.
+    */
+   public void setMaxPoolSize(int maxPoolSize) {
+      this.maxPoolSize = maxPoolSize;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpUnknownFilterType.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpUnknownFilterType.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpUnknownFilterType.java
new file mode 100644
index 0000000..8a4ce6b
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpUnknownFilterType.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.activemq.transport.amqp.client;
+
+import org.apache.qpid.proton.amqp.DescribedType;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.UnsignedLong;
+
+/**
+ * A Described Type wrapper for an unsupported filter that the broker should ignore.
+ */
+public class AmqpUnknownFilterType implements DescribedType {
+
+   public static final AmqpUnknownFilterType UNKOWN_FILTER = new AmqpUnknownFilterType();
+
+   public static final UnsignedLong UNKNOWN_FILTER_CODE = UnsignedLong.valueOf(0x0000468C00000099L);
+   public static final Symbol UNKNOWN_FILTER_NAME = Symbol.valueOf("apache.org:unkown-filter:string");
+   public static final Object[] UNKNOWN_FILTER_IDS = new Object[]{UNKNOWN_FILTER_CODE, UNKNOWN_FILTER_NAME};
+
+   private final String payload;
+
+   public AmqpUnknownFilterType() {
+      this.payload = "UnknownFilter{}";
+   }
+
+   @Override
+   public Object getDescriptor() {
+      return UNKNOWN_FILTER_CODE;
+   }
+
+   @Override
+   public Object getDescribed() {
+      return this.payload;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpValidator.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpValidator.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpValidator.java
new file mode 100644
index 0000000..5f46cb6
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpValidator.java
@@ -0,0 +1,101 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import org.apache.qpid.proton.engine.Connection;
+import org.apache.qpid.proton.engine.Receiver;
+import org.apache.qpid.proton.engine.Sender;
+import org.apache.qpid.proton.engine.Session;
+
+/**
+ * Abstract base for a validation hook that is used in tests to check
+ * the state of a remote resource after a variety of lifecycle events.
+ */
+public class AmqpValidator {
+
+   private boolean valid = true;
+   private String errorMessage;
+
+   public void inspectOpenedResource(Connection connection) {
+
+   }
+
+   public void inspectOpenedResource(Session session) {
+
+   }
+
+   public void inspectOpenedResource(Sender sender) {
+
+   }
+
+   public void inspectOpenedResource(Receiver receiver) {
+
+   }
+
+   public void inspectClosedResource(Connection remoteConnection) {
+
+   }
+
+   public void inspectClosedResource(Session session) {
+
+   }
+
+   public void inspectClosedResource(Sender sender) {
+
+   }
+
+   public void inspectClosedResource(Receiver receiver) {
+
+   }
+
+   public void inspectDetachedResource(Sender sender) {
+
+   }
+
+   public void inspectDetachedResource(Receiver receiver) {
+
+   }
+
+   public boolean isValid() {
+      return valid;
+   }
+
+   protected void setValid(boolean valid) {
+      this.valid = valid;
+   }
+
+   public String getErrorMessage() {
+      return errorMessage;
+   }
+
+   protected void setErrorMessage(String errorMessage) {
+      this.errorMessage = errorMessage;
+   }
+
+   protected void markAsInvalid(String errorMessage) {
+      if (valid) {
+         setValid(false);
+         setErrorMessage(errorMessage);
+      }
+   }
+
+   public void assertValid() {
+      if (!isValid()) {
+         throw new AssertionError(errorMessage);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AbstractMechanism.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AbstractMechanism.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AbstractMechanism.java
new file mode 100644
index 0000000..011fba7
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AbstractMechanism.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.activemq.transport.amqp.client.sasl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for SASL Authentication Mechanism that implements the basic
+ * methods of a Mechanism class.
+ */
+public abstract class AbstractMechanism implements Mechanism {
+
+   protected static final byte[] EMPTY = new byte[0];
+
+   private String username;
+   private String password;
+   private String authzid;
+   private Map<String, Object> properties = new HashMap<>();
+
+   @Override
+   public int compareTo(Mechanism other) {
+
+      if (getPriority() < other.getPriority()) {
+         return -1;
+      }
+      else if (getPriority() > other.getPriority()) {
+         return 1;
+      }
+
+      return 0;
+   }
+
+   @Override
+   public void setUsername(String value) {
+      this.username = value;
+   }
+
+   @Override
+   public String getUsername() {
+      return username;
+   }
+
+   @Override
+   public void setPassword(String value) {
+      this.password = value;
+   }
+
+   @Override
+   public String getPassword() {
+      return this.password;
+   }
+
+   @Override
+   public void setProperties(Map<String, Object> properties) {
+      this.properties = properties;
+   }
+
+   @Override
+   public Map<String, Object> getProperties() {
+      return this.properties;
+   }
+
+   @Override
+   public String toString() {
+      return "SASL-" + getName();
+   }
+
+   @Override
+   public String getAuthzid() {
+      return authzid;
+   }
+
+   @Override
+   public void setAuthzid(String authzid) {
+      this.authzid = authzid;
+   }
+
+   @Override
+   public boolean isApplicable(String username, String password) {
+      return true;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AnonymousMechanism.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AnonymousMechanism.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AnonymousMechanism.java
new file mode 100644
index 0000000..c3d36aa
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/AnonymousMechanism.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.activemq.transport.amqp.client.sasl;
+
+/**
+ * Implements the Anonymous SASL authentication mechanism.
+ */
+public class AnonymousMechanism extends AbstractMechanism {
+
+   @Override
+   public byte[] getInitialResponse() {
+      return EMPTY;
+   }
+
+   @Override
+   public byte[] getChallengeResponse(byte[] challenge) {
+      return EMPTY;
+   }
+
+   @Override
+   public int getPriority() {
+      return PRIORITY.LOWEST.getValue();
+   }
+
+   @Override
+   public String getName() {
+      return "ANONYMOUS";
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/CramMD5Mechanism.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/CramMD5Mechanism.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/CramMD5Mechanism.java
new file mode 100644
index 0000000..4821314
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/CramMD5Mechanism.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.activemq.transport.amqp.client.sasl;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.sasl.SaslException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Implements the SASL PLAIN authentication Mechanism.
+ *
+ * User name and Password values are sent without being encrypted.
+ */
+public class CramMD5Mechanism extends AbstractMechanism {
+
+   private static final String ASCII = "ASCII";
+   private static final String HMACMD5 = "HMACMD5";
+   private boolean sentResponse;
+
+   @Override
+   public int getPriority() {
+      return PRIORITY.HIGH.getValue();
+   }
+
+   @Override
+   public String getName() {
+      return "CRAM-MD5";
+   }
+
+   @Override
+   public byte[] getInitialResponse() {
+      return EMPTY;
+   }
+
+   @Override
+   public byte[] getChallengeResponse(byte[] challenge) throws SaslException {
+      if (!sentResponse && challenge != null && challenge.length != 0) {
+         try {
+            SecretKeySpec key = new SecretKeySpec(getPassword().getBytes(ASCII), HMACMD5);
+            Mac mac = Mac.getInstance(HMACMD5);
+            mac.init(key);
+
+            byte[] bytes = mac.doFinal(challenge);
+
+            StringBuffer hash = new StringBuffer(getUsername());
+            hash.append(' ');
+            for (int i = 0; i < bytes.length; i++) {
+               String hex = Integer.toHexString(0xFF & bytes[i]);
+               if (hex.length() == 1) {
+                  hash.append('0');
+               }
+               hash.append(hex);
+            }
+
+            sentResponse = true;
+            return hash.toString().getBytes(ASCII);
+         }
+         catch (UnsupportedEncodingException e) {
+            throw new SaslException("Unable to utilise required encoding", e);
+         }
+         catch (InvalidKeyException e) {
+            throw new SaslException("Unable to utilise key", e);
+         }
+         catch (NoSuchAlgorithmException e) {
+            throw new SaslException("Unable to utilise required algorithm", e);
+         }
+      }
+      else {
+         return EMPTY;
+      }
+   }
+
+   @Override
+   public boolean isApplicable(String username, String password) {
+      return username != null && username.length() > 0 && password != null && password.length() > 0;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/Mechanism.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/Mechanism.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/Mechanism.java
new file mode 100644
index 0000000..a79406f
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/Mechanism.java
@@ -0,0 +1,143 @@
+/**
+ * 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.activemq.transport.amqp.client.sasl;
+
+import javax.security.sasl.SaslException;
+import java.util.Map;
+
+/**
+ * Interface for all SASL authentication mechanism implementations.
+ */
+public interface Mechanism extends Comparable<Mechanism> {
+
+   /**
+    * Relative priority values used to arrange the found SASL
+    * mechanisms in a preferred order where the level of security
+    * generally defines the preference.
+    */
+   enum PRIORITY {
+      LOWEST(0),
+      LOW(1),
+      MEDIUM(2),
+      HIGH(3),
+      HIGHEST(4);
+
+      private final int value;
+
+      PRIORITY(int value) {
+         this.value = value;
+      }
+
+      public int getValue() {
+         return value;
+      }
+   };
+
+   /**
+    * @return return the relative priority of this SASL mechanism.
+    */
+   int getPriority();
+
+   /**
+    * @return the well known name of this SASL mechanism.
+    */
+   String getName();
+
+   /**
+    * @return the response buffer used to answer the initial SASL cycle.
+    * @throws SaslException if an error occurs computing the response.
+    */
+   byte[] getInitialResponse() throws SaslException;
+
+   /**
+    * Create a response based on a given challenge from the remote peer.
+    *
+    * @param challenge the challenge that this Mechanism should response to.
+    * @return the response that answers the given challenge.
+    * @throws SaslException if an error occurs computing the response.
+    */
+   byte[] getChallengeResponse(byte[] challenge) throws SaslException;
+
+   /**
+    * Sets the user name value for this Mechanism.  The Mechanism can ignore this
+    * value if it does not utilize user name in it's authentication processing.
+    *
+    * @param username The user name given.
+    */
+   void setUsername(String value);
+
+   /**
+    * Returns the configured user name value for this Mechanism.
+    *
+    * @return the currently set user name value for this Mechanism.
+    */
+   String getUsername();
+
+   /**
+    * Sets the password value for this Mechanism.  The Mechanism can ignore this
+    * value if it does not utilize a password in it's authentication processing.
+    *
+    * @param username The user name given.
+    */
+   void setPassword(String value);
+
+   /**
+    * Returns the configured password value for this Mechanism.
+    *
+    * @return the currently set password value for this Mechanism.
+    */
+   String getPassword();
+
+   /**
+    * Sets any additional Mechanism specific properties using a Map<String, Object>
+    *
+    * @param options the map of additional properties that this Mechanism should utilize.
+    */
+   void setProperties(Map<String, Object> options);
+
+   /**
+    * The currently set Properties for this Mechanism.
+    *
+    * @return the current set of configuration Properties for this Mechanism.
+    */
+   Map<String, Object> getProperties();
+
+   /**
+    * Using the configured credentials, check if the mechanism applies or not.
+    *
+    * @param username The user name that will be used with this mechanism
+    * @param password The password that will be used with this mechanism
+    * @return true if the mechanism works with the provided credentials or not.
+    */
+   boolean isApplicable(String username, String password);
+
+   /**
+    * Get the currently configured Authentication ID.
+    *
+    * @return the currently set Authentication ID.
+    */
+   String getAuthzid();
+
+   /**
+    * Sets an Authentication ID that some mechanism can use during the
+    * challenge response phase.
+    *
+    * @param authzid The Authentication ID to use.
+    */
+   void setAuthzid(String authzid);
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/PlainMechanism.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/PlainMechanism.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/PlainMechanism.java
new file mode 100644
index 0000000..d9b3ba3
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/PlainMechanism.java
@@ -0,0 +1,76 @@
+/**
+ * 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.activemq.transport.amqp.client.sasl;
+
+/**
+ * Implements the SASL PLAIN authentication Mechanism.
+ *
+ * User name and Password values are sent without being encrypted.
+ */
+public class PlainMechanism extends AbstractMechanism {
+
+   public static final String MECH_NAME = "PLAIN";
+
+   @Override
+   public int getPriority() {
+      return PRIORITY.MEDIUM.getValue();
+   }
+
+   @Override
+   public String getName() {
+      return MECH_NAME;
+   }
+
+   @Override
+   public byte[] getInitialResponse() {
+
+      String authzid = getAuthzid();
+      String username = getUsername();
+      String password = getPassword();
+
+      if (authzid == null) {
+         authzid = "";
+      }
+
+      if (username == null) {
+         username = "";
+      }
+
+      if (password == null) {
+         password = "";
+      }
+
+      byte[] authzidBytes = authzid.getBytes();
+      byte[] usernameBytes = username.getBytes();
+      byte[] passwordBytes = password.getBytes();
+      byte[] data = new byte[authzidBytes.length + 1 + usernameBytes.length + 1 + passwordBytes.length];
+      System.arraycopy(authzidBytes, 0, data, 0, authzidBytes.length);
+      System.arraycopy(usernameBytes, 0, data, 1 + authzidBytes.length, usernameBytes.length);
+      System.arraycopy(passwordBytes, 0, data, 2 + authzidBytes.length + usernameBytes.length, passwordBytes.length);
+      return data;
+   }
+
+   @Override
+   public byte[] getChallengeResponse(byte[] challenge) {
+      return EMPTY;
+   }
+
+   @Override
+   public boolean isApplicable(String username, String password) {
+      return username != null && username.length() > 0 && password != null && password.length() > 0;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/SaslAuthenticator.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/SaslAuthenticator.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/SaslAuthenticator.java
new file mode 100644
index 0000000..5c25fae
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/sasl/SaslAuthenticator.java
@@ -0,0 +1,182 @@
+/**
+ * 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.activemq.transport.amqp.client.sasl;
+
+import javax.security.sasl.SaslException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.qpid.proton.engine.Sasl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manage the SASL authentication process
+ */
+public class SaslAuthenticator {
+
+   private static final Logger LOG = LoggerFactory.getLogger(SaslAuthenticator.class);
+
+   private final Sasl sasl;
+   private final String username;
+   private final String password;
+   private final String authzid;
+   private Mechanism mechanism;
+   private String mechanismRestriction;
+
+   /**
+    * Create the authenticator and initialize it.
+    *
+    * @param sasl                 The Proton SASL entry point this class will use to manage the authentication.
+    * @param username             The user name that will be used to authenticate.
+    * @param password             The password that will be used to authenticate.
+    * @param authzid              The authzid used when authenticating (currently only with PLAIN)
+    * @param mechanismRestriction A particular mechanism to use (if offered by the server) or null to allow selection.
+    */
+   public SaslAuthenticator(Sasl sasl, String username, String password, String authzid, String mechanismRestriction) {
+      this.sasl = sasl;
+      this.username = username;
+      this.password = password;
+      this.authzid = authzid;
+      this.mechanismRestriction = mechanismRestriction;
+   }
+
+   /**
+    * Process the SASL authentication cycle until such time as an outcome is determine. This
+    * method must be called by the managing entity until the return value is true indicating a
+    * successful authentication or a JMSSecurityException is thrown indicating that the
+    * handshake failed.
+    *
+    * @throws SecurityException
+    */
+   public boolean authenticate() throws SecurityException {
+      switch (sasl.getState()) {
+         case PN_SASL_IDLE:
+            handleSaslInit();
+            break;
+         case PN_SASL_STEP:
+            handleSaslStep();
+            break;
+         case PN_SASL_FAIL:
+            handleSaslFail();
+            break;
+         case PN_SASL_PASS:
+            return true;
+         default:
+      }
+
+      return false;
+   }
+
+   private void handleSaslInit() throws SecurityException {
+      try {
+         String[] remoteMechanisms = sasl.getRemoteMechanisms();
+         if (remoteMechanisms != null && remoteMechanisms.length != 0) {
+            mechanism = findMatchingMechanism(remoteMechanisms);
+            if (mechanism != null) {
+               mechanism.setUsername(username);
+               mechanism.setPassword(password);
+               mechanism.setAuthzid(authzid);
+               // TODO - set additional options from URI.
+               // TODO - set a host value.
+
+               sasl.setMechanisms(mechanism.getName());
+               byte[] response = mechanism.getInitialResponse();
+               if (response != null && response.length != 0) {
+                  sasl.send(response, 0, response.length);
+               }
+            }
+            else {
+               // TODO - Better error message.
+               throw new SecurityException("Could not find a matching SASL mechanism for the remote peer.");
+            }
+         }
+      }
+      catch (SaslException se) {
+         // TODO - Better error message.
+         SecurityException jmsse = new SecurityException("Exception while processing SASL init.");
+         jmsse.initCause(se);
+         throw jmsse;
+      }
+   }
+
+   private Mechanism findMatchingMechanism(String... remoteMechanisms) {
+
+      Mechanism match = null;
+      List<Mechanism> found = new ArrayList<>();
+
+      for (String remoteMechanism : remoteMechanisms) {
+         if (mechanismRestriction != null && !mechanismRestriction.equals(remoteMechanism)) {
+            LOG.debug("Skipping {} mechanism because it is not the configured mechanism restriction {}", remoteMechanism, mechanismRestriction);
+            continue;
+         }
+
+         Mechanism mechanism = null;
+         if (remoteMechanism.equalsIgnoreCase("PLAIN")) {
+            mechanism = new PlainMechanism();
+         }
+         else if (remoteMechanism.equalsIgnoreCase("ANONYMOUS")) {
+            mechanism = new AnonymousMechanism();
+         }
+         else if (remoteMechanism.equalsIgnoreCase("CRAM-MD5")) {
+            mechanism = new CramMD5Mechanism();
+         }
+         else {
+            LOG.debug("Unknown remote mechanism {}, skipping", remoteMechanism);
+            continue;
+         }
+
+         if (mechanism.isApplicable(username, password)) {
+            found.add(mechanism);
+         }
+      }
+
+      if (!found.isEmpty()) {
+         // Sorts by priority using Mechanism comparison and return the last value in
+         // list which is the Mechanism deemed to be the highest priority match.
+         Collections.sort(found);
+         match = found.get(found.size() - 1);
+      }
+
+      LOG.info("Best match for SASL auth was: {}", match);
+
+      return match;
+   }
+
+   private void handleSaslStep() throws SecurityException {
+      try {
+         if (sasl.pending() != 0) {
+            byte[] challenge = new byte[sasl.pending()];
+            sasl.recv(challenge, 0, challenge.length);
+            byte[] response = mechanism.getChallengeResponse(challenge);
+            sasl.send(response, 0, response.length);
+         }
+      }
+      catch (SaslException se) {
+         // TODO - Better error message.
+         SecurityException jmsse = new SecurityException("Exception while processing SASL step.");
+         jmsse.initCause(se);
+         throw jmsse;
+      }
+   }
+
+   private void handleSaslFail() throws SecurityException {
+      // TODO - Better error message.
+      throw new SecurityException("Client failed to authenticate");
+   }
+}


[9/9] activemq-artemis git commit: ARTEMIS-637 Port 5.x AMQP test client

Posted by an...@apache.org.
ARTEMIS-637 Port 5.x AMQP test client


Project: http://git-wip-us.apache.org/repos/asf/activemq-artemis/repo
Commit: http://git-wip-us.apache.org/repos/asf/activemq-artemis/commit/df41a60e
Tree: http://git-wip-us.apache.org/repos/asf/activemq-artemis/tree/df41a60e
Diff: http://git-wip-us.apache.org/repos/asf/activemq-artemis/diff/df41a60e

Branch: refs/heads/master
Commit: df41a60e21783f33f435ef3a9efa54f9dab146d7
Parents: 5695164
Author: Martyn Taylor <mt...@redhat.com>
Authored: Fri Jul 15 18:03:31 2016 +0100
Committer: Andy Taylor <an...@gmail.com>
Committed: Wed Jul 20 10:33:44 2016 +0100

----------------------------------------------------------------------
 tests/artemis-test-support/pom.xml              |  57 ++
 .../transport/amqp/AmqpProtocolException.java   |  62 ++
 .../activemq/transport/amqp/AmqpSupport.java    | 206 ++++
 .../amqp/client/AmqpAbstractResource.java       | 321 +++++++
 .../transport/amqp/client/AmqpClient.java       | 245 +++++
 .../transport/amqp/client/AmqpConnection.java   | 720 ++++++++++++++
 .../amqp/client/AmqpConnectionListener.java     |  31 +
 .../client/AmqpDefaultConnectionListener.java   |  28 +
 .../transport/amqp/client/AmqpEventSink.java    |  69 ++
 .../amqp/client/AmqpJmsSelectorFilter.java      |  48 +
 .../transport/amqp/client/AmqpMessage.java      | 515 ++++++++++
 .../amqp/client/AmqpNoLocalFilter.java          |  45 +
 .../transport/amqp/client/AmqpReceiver.java     | 946 +++++++++++++++++++
 .../amqp/client/AmqpRedirectedException.java    |  61 ++
 .../transport/amqp/client/AmqpResource.java     | 108 +++
 .../transport/amqp/client/AmqpSender.java       | 452 +++++++++
 .../transport/amqp/client/AmqpSession.java      | 454 +++++++++
 .../transport/amqp/client/AmqpSupport.java      | 195 ++++
 .../amqp/client/AmqpTransactionContext.java     | 261 +++++
 .../amqp/client/AmqpTransactionCoordinator.java | 262 +++++
 .../amqp/client/AmqpTransactionId.java          |  98 ++
 .../amqp/client/AmqpTransferTagGenerator.java   | 104 ++
 .../amqp/client/AmqpUnknownFilterType.java      |  49 +
 .../transport/amqp/client/AmqpValidator.java    | 101 ++
 .../amqp/client/sasl/AbstractMechanism.java     |  97 ++
 .../amqp/client/sasl/AnonymousMechanism.java    |  43 +
 .../amqp/client/sasl/CramMD5Mechanism.java      |  94 ++
 .../transport/amqp/client/sasl/Mechanism.java   | 143 +++
 .../amqp/client/sasl/PlainMechanism.java        |  76 ++
 .../amqp/client/sasl/SaslAuthenticator.java     | 182 ++++
 .../client/transport/NettyTcpTransport.java     | 402 ++++++++
 .../amqp/client/transport/NettyTransport.java   |  52 +
 .../client/transport/NettyTransportFactory.java |  80 ++
 .../transport/NettyTransportListener.java       |  46 +
 .../client/transport/NettyTransportOptions.java | 177 ++++
 .../transport/NettyTransportSslOptions.java     | 284 ++++++
 .../client/transport/NettyTransportSupport.java | 288 ++++++
 .../amqp/client/transport/NettyWSTransport.java | 472 +++++++++
 .../PartialPooledByteBufAllocator.java          | 134 +++
 .../client/transport/X509AliasKeyManager.java   |  86 ++
 .../transport/amqp/client/util/AsyncResult.java |  46 +
 .../amqp/client/util/ClientFuture.java          | 110 +++
 .../util/ClientFutureSynchronization.java       |  30 +
 .../amqp/client/util/IOExceptionSupport.java    |  45 +
 .../transport/amqp/client/util/IdGenerator.java | 274 ++++++
 .../amqp/client/util/NoOpAsyncResult.java       |  40 +
 .../amqp/client/util/PropertyUtil.java          | 533 +++++++++++
 .../amqp/client/util/StringArrayConverter.java  |  64 ++
 .../amqp/client/util/TypeConversionSupport.java | 218 +++++
 .../client/util/UnmodifiableConnection.java     | 202 ++++
 .../amqp/client/util/UnmodifiableDelivery.java  | 170 ++++
 .../amqp/client/util/UnmodifiableLink.java      | 276 ++++++
 .../amqp/client/util/UnmodifiableReceiver.java  |  59 ++
 .../amqp/client/util/UnmodifiableSender.java    |  45 +
 .../amqp/client/util/UnmodifiableSession.java   | 150 +++
 .../amqp/client/util/UnmodifiableTransport.java | 274 ++++++
 .../amqp/client/util/WrappedAsyncResult.java    |  59 ++
 tests/integration-tests/pom.xml                 |   5 +
 tests/pom.xml                                   |   8 +
 59 files changed, 10702 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/pom.xml
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/pom.xml b/tests/artemis-test-support/pom.xml
new file mode 100644
index 0000000..ec0c49d
--- /dev/null
+++ b/tests/artemis-test-support/pom.xml
@@ -0,0 +1,57 @@
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+   <modelVersion>4.0.0</modelVersion>
+   <parent>
+      <groupId>org.apache.activemq.tests</groupId>
+      <artifactId>artemis-tests-pom</artifactId>
+      <version>1.4.0-SNAPSHOT</version>
+   </parent>
+
+   <artifactId>artemis-test-support</artifactId>
+   <packaging>jar</packaging>
+   <name>ActiveMQ Artemis Test Support</name>
+
+   <properties>
+      <activemq.basedir>${project.basedir}/../..</activemq.basedir>
+   </properties>
+
+   <dependencies>
+      <dependency>
+         <groupId>org.apache.qpid</groupId>
+         <artifactId>proton-j</artifactId>
+      </dependency>
+      <dependency>
+         <groupId>org.apache.qpid</groupId>
+         <artifactId>qpid-jms-client</artifactId>
+      </dependency>
+      <dependency>
+         <groupId>org.slf4j</groupId>
+         <artifactId>slf4j-api</artifactId>
+      </dependency>
+      <dependency>
+         <groupId>io.netty</groupId>
+         <artifactId>netty-all</artifactId>
+      </dependency>
+      <dependency>
+         <groupId>org.apache.activemq</groupId>
+         <artifactId>activemq-client</artifactId>
+      </dependency>
+   </dependencies>
+
+</project>

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpProtocolException.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpProtocolException.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpProtocolException.java
new file mode 100644
index 0000000..6e58417
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpProtocolException.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.activemq.transport.amqp;
+
+import java.io.IOException;
+
+public class AmqpProtocolException extends IOException {
+
+   private static final long serialVersionUID = -2869735532997332242L;
+
+   private final String symbolicName;
+   private final boolean fatal;
+
+   public AmqpProtocolException() {
+      this(null);
+   }
+
+   public AmqpProtocolException(String s) {
+      this(s, false);
+   }
+
+   public AmqpProtocolException(String s, boolean fatal) {
+      this(s, fatal, null);
+   }
+
+   public AmqpProtocolException(String s, String msg) {
+      this(s, msg, false, null);
+   }
+
+   public AmqpProtocolException(String s, boolean fatal, Throwable cause) {
+      this("error", s, fatal, cause);
+   }
+
+   public AmqpProtocolException(String symbolicName, String s, boolean fatal, Throwable cause) {
+      super(s);
+      this.symbolicName = symbolicName;
+      this.fatal = fatal;
+      initCause(cause);
+   }
+
+   public boolean isFatal() {
+      return fatal;
+   }
+
+   public String getSymbolicName() {
+      return symbolicName;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpSupport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpSupport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpSupport.java
new file mode 100644
index 0000000..cde4def
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/AmqpSupport.java
@@ -0,0 +1,206 @@
+/**
+ * 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.activemq.transport.amqp;
+
+import java.nio.ByteBuffer;
+import java.util.AbstractMap;
+import java.util.Map;
+
+import org.apache.activemq.command.ActiveMQDestination;
+import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.DescribedType;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.UnsignedLong;
+import org.apache.qpid.proton.amqp.transaction.Coordinator;
+import org.fusesource.hawtbuf.Buffer;
+
+/**
+ * Set of useful methods and definitions used in the AMQP protocol handling
+ */
+public class AmqpSupport {
+
+   // Identification values used to locating JMS selector types.
+   public static final UnsignedLong JMS_SELECTOR_CODE = UnsignedLong.valueOf(0x0000468C00000004L);
+   public static final Symbol JMS_SELECTOR_NAME = Symbol.valueOf("apache.org:selector-filter:string");
+   public static final Object[] JMS_SELECTOR_FILTER_IDS = new Object[]{JMS_SELECTOR_CODE, JMS_SELECTOR_NAME};
+   public static final UnsignedLong NO_LOCAL_CODE = UnsignedLong.valueOf(0x0000468C00000003L);
+   public static final Symbol NO_LOCAL_NAME = Symbol.valueOf("apache.org:no-local-filter:list");
+   public static final Object[] NO_LOCAL_FILTER_IDS = new Object[]{NO_LOCAL_CODE, NO_LOCAL_NAME};
+
+   // Capabilities used to identify destination type in some requests.
+   public static final Symbol TEMP_QUEUE_CAPABILITY = Symbol.valueOf("temporary-queue");
+   public static final Symbol TEMP_TOPIC_CAPABILITY = Symbol.valueOf("temporary-topic");
+
+   // Symbols used to announce connection information to remote peer.
+   public static final Symbol INVALID_FIELD = Symbol.valueOf("invalid-field");
+   public static final Symbol CONTAINER_ID = Symbol.valueOf("container-id");
+
+   // Symbols used to announce connection information to remote peer.
+   public static final Symbol ANONYMOUS_RELAY = Symbol.valueOf("ANONYMOUS-RELAY");
+   public static final Symbol QUEUE_PREFIX = Symbol.valueOf("queue-prefix");
+   public static final Symbol TOPIC_PREFIX = Symbol.valueOf("topic-prefix");
+   public static final Symbol CONNECTION_OPEN_FAILED = Symbol.valueOf("amqp:connection-establishment-failed");
+   public static final Symbol PRODUCT = Symbol.valueOf("product");
+   public static final Symbol VERSION = Symbol.valueOf("version");
+   public static final Symbol PLATFORM = Symbol.valueOf("platform");
+
+   // Symbols used in configuration of newly opened links.
+   public static final Symbol COPY = Symbol.getSymbol("copy");
+
+   // Lifetime policy symbols
+   public static final Symbol LIFETIME_POLICY = Symbol.valueOf("lifetime-policy");
+
+   /**
+    * Search for a given Symbol in a given array of Symbol object.
+    *
+    * @param symbols the set of Symbols to search.
+    * @param key     the value to try and find in the Symbol array.
+    * @return true if the key is found in the given Symbol array.
+    */
+   public static boolean contains(Symbol[] symbols, Symbol key) {
+      if (symbols == null || symbols.length == 0) {
+         return false;
+      }
+
+      for (Symbol symbol : symbols) {
+         if (symbol.equals(key)) {
+            return true;
+         }
+      }
+
+      return false;
+   }
+
+   /**
+    * Search for a particular filter using a set of known indentification values
+    * in the Map of filters.
+    *
+    * @param filters   The filters map that should be searched.
+    * @param filterIds The aliases for the target filter to be located.
+    * @return the filter if found in the mapping or null if not found.
+    */
+   public static Map.Entry<Symbol, DescribedType> findFilter(Map<Symbol, Object> filters, Object[] filterIds) {
+
+      if (filterIds == null || filterIds.length == 0) {
+         throw new IllegalArgumentException("Invalid empty Filter Ids array passed: ");
+      }
+
+      if (filters == null || filters.isEmpty()) {
+         return null;
+      }
+
+      for (Map.Entry<Symbol, Object> filter : filters.entrySet()) {
+         if (filter.getValue() instanceof DescribedType) {
+            DescribedType describedType = ((DescribedType) filter.getValue());
+            Object descriptor = describedType.getDescriptor();
+
+            for (Object filterId : filterIds) {
+               if (descriptor.equals(filterId)) {
+                  return new AbstractMap.SimpleImmutableEntry<>(filter.getKey(), describedType);
+               }
+            }
+         }
+      }
+
+      return null;
+   }
+
+   /**
+    * Conversion from Java ByteBuffer to a HawtBuf buffer.
+    *
+    * @param data the ByteBuffer instance to convert.
+    * @return a new HawtBuf buffer converted from the given ByteBuffer.
+    */
+   public static Buffer toBuffer(ByteBuffer data) {
+      if (data == null) {
+         return null;
+      }
+
+      Buffer rc;
+
+      if (data.isDirect()) {
+         rc = new Buffer(data.remaining());
+         data.get(rc.data);
+      }
+      else {
+         rc = new Buffer(data);
+         data.position(data.position() + data.remaining());
+      }
+
+      return rc;
+   }
+
+   /**
+    * Given a long value, convert it to a byte array for marshalling.
+    *
+    * @param value the value to convert.
+    * @return a new byte array that holds the big endian value of the long.
+    */
+   public static byte[] toBytes(long value) {
+      Buffer buffer = new Buffer(8);
+      buffer.bigEndianEditor().writeLong(value);
+      return buffer.data;
+   }
+
+   /**
+    * Converts a Binary value to a long assuming that the contained value is
+    * stored in Big Endian encoding.
+    *
+    * @param value the Binary object whose payload is converted to a long.
+    * @return a long value constructed from the bytes of the Binary instance.
+    */
+   public static long toLong(Binary value) {
+      Buffer buffer = new Buffer(value.getArray(), value.getArrayOffset(), value.getLength());
+      return buffer.bigEndianEditor().readLong();
+   }
+
+   /**
+    * Given an AMQP endpoint, deduce the appropriate ActiveMQDestination type and create
+    * a new instance.  By default if the endpoint address does not carry the standard prefix
+    * value then we default to a Queue type destination.  If the endpoint is null or is an
+    * AMQP Coordinator type endpoint this method returns null to indicate no destination
+    * can be mapped.
+    *
+    * @param endpoint the AMQP endpoint to construct an ActiveMQDestination from.
+    * @return a new ActiveMQDestination that best matches the address of the given endpoint
+    * @throws AmqpProtocolException if an error occurs while deducing the destination type.
+    */
+   public static ActiveMQDestination createDestination(Object endpoint) throws AmqpProtocolException {
+      if (endpoint == null) {
+         return null;
+      }
+      else if (endpoint instanceof Coordinator) {
+         return null;
+      }
+      else if (endpoint instanceof org.apache.qpid.proton.amqp.messaging.Terminus) {
+         org.apache.qpid.proton.amqp.messaging.Terminus terminus = (org.apache.qpid.proton.amqp.messaging.Terminus) endpoint;
+         if (terminus.getAddress() == null || terminus.getAddress().length() == 0) {
+            if (terminus instanceof org.apache.qpid.proton.amqp.messaging.Source) {
+               throw new AmqpProtocolException("amqp:invalid-field", "source address not set");
+            }
+            else {
+               throw new AmqpProtocolException("amqp:invalid-field", "target address not set");
+            }
+         }
+
+         return ActiveMQDestination.createDestination(terminus.getAddress(), ActiveMQDestination.QUEUE_TYPE);
+      }
+      else {
+         throw new RuntimeException("Unexpected terminus type: " + endpoint);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpAbstractResource.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpAbstractResource.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpAbstractResource.java
new file mode 100644
index 0000000..b99c56b
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpAbstractResource.java
@@ -0,0 +1,321 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import java.io.IOException;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.qpid.proton.engine.Endpoint;
+import org.apache.qpid.proton.engine.EndpointState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base for all AmqpResource implementations to extend.
+ *
+ * This abstract class wraps up the basic state management bits so that the concrete
+ * object don't have to reproduce it.  Provides hooks for the subclasses to initialize
+ * and shutdown.
+ */
+public abstract class AmqpAbstractResource<E extends Endpoint> implements AmqpResource {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpAbstractResource.class);
+
+   protected AsyncResult openRequest;
+   protected AsyncResult closeRequest;
+
+   private AmqpValidator amqpStateInspector;
+
+   private E endpoint;
+
+   @Override
+   public void open(AsyncResult request) {
+      this.openRequest = request;
+      doOpen();
+      getEndpoint().setContext(this);
+   }
+
+   @Override
+   public boolean isOpen() {
+      return getEndpoint().getRemoteState() == EndpointState.ACTIVE;
+   }
+
+   @Override
+   public void opened() {
+      if (this.openRequest != null) {
+         this.openRequest.onSuccess();
+         this.openRequest = null;
+      }
+   }
+
+   @Override
+   public void detach(AsyncResult request) {
+      // If already closed signal success or else the caller might never get notified.
+      if (getEndpoint().getLocalState() == EndpointState.CLOSED || getEndpoint().getRemoteState() == EndpointState.CLOSED) {
+
+         if (getEndpoint().getLocalState() != EndpointState.CLOSED) {
+            doDetach();
+            getEndpoint().free();
+         }
+
+         request.onSuccess();
+      }
+      else {
+         this.closeRequest = request;
+         doDetach();
+      }
+   }
+
+   @Override
+   public void close(AsyncResult request) {
+      // If already closed signal success or else the caller might never get notified.
+      if (getEndpoint().getLocalState() == EndpointState.CLOSED || getEndpoint().getRemoteState() == EndpointState.CLOSED) {
+
+         if (getEndpoint().getLocalState() != EndpointState.CLOSED) {
+            doClose();
+            getEndpoint().free();
+         }
+
+         request.onSuccess();
+      }
+      else {
+         this.closeRequest = request;
+         doClose();
+      }
+   }
+
+   @Override
+   public boolean isClosed() {
+      return getEndpoint().getLocalState() == EndpointState.CLOSED;
+   }
+
+   @Override
+   public void closed() {
+      getEndpoint().close();
+      getEndpoint().free();
+
+      if (this.closeRequest != null) {
+         this.closeRequest.onSuccess();
+         this.closeRequest = null;
+      }
+   }
+
+   @Override
+   public void failed() {
+      failed(new Exception("Remote request failed."));
+   }
+
+   @Override
+   public void failed(Exception cause) {
+      if (openRequest != null) {
+         if (endpoint != null) {
+            // TODO: if this is a producer/consumer link then we may only be detached,
+            // rather than fully closed, and should respond appropriately.
+            endpoint.close();
+         }
+         openRequest.onFailure(cause);
+         openRequest = null;
+      }
+
+      if (closeRequest != null) {
+         closeRequest.onFailure(cause);
+         closeRequest = null;
+      }
+   }
+
+   @Override
+   public void remotelyClosed(AmqpConnection connection) {
+      Exception error = AmqpSupport.convertToException(getEndpoint().getRemoteCondition());
+
+      if (endpoint != null) {
+         // TODO: if this is a producer/consumer link then we may only be detached,
+         // rather than fully closed, and should respond appropriately.
+         endpoint.close();
+      }
+
+      LOG.info("Resource {} was remotely closed", this);
+
+      connection.fireClientException(error);
+   }
+
+   @Override
+   public void locallyClosed(AmqpConnection connection, Exception error) {
+      if (endpoint != null) {
+         // TODO: if this is a producer/consumer link then we may only be detached,
+         // rather than fully closed, and should respond appropriately.
+         endpoint.close();
+      }
+
+      LOG.info("Resource {} was locally closed", this);
+
+      connection.fireClientException(error);
+   }
+
+   public E getEndpoint() {
+      return this.endpoint;
+   }
+
+   public void setEndpoint(E endpoint) {
+      this.endpoint = endpoint;
+   }
+
+   public AmqpValidator getStateInspector() {
+      return amqpStateInspector;
+   }
+
+   public void setStateInspector(AmqpValidator stateInspector) {
+      if (stateInspector == null) {
+         stateInspector = new AmqpValidator();
+      }
+
+      this.amqpStateInspector = stateInspector;
+   }
+
+   public EndpointState getLocalState() {
+      if (getEndpoint() == null) {
+         return EndpointState.UNINITIALIZED;
+      }
+      return getEndpoint().getLocalState();
+   }
+
+   public EndpointState getRemoteState() {
+      if (getEndpoint() == null) {
+         return EndpointState.UNINITIALIZED;
+      }
+      return getEndpoint().getRemoteState();
+   }
+
+   public boolean hasRemoteError() {
+      return getEndpoint().getRemoteCondition().getCondition() != null;
+   }
+
+   @Override
+   public void processRemoteOpen(AmqpConnection connection) throws IOException {
+      doOpenInspection();
+      doOpenCompletion();
+   }
+
+   @Override
+   public void processRemoteDetach(AmqpConnection connection) throws IOException {
+      doDetachedInspection();
+      if (isAwaitingClose()) {
+         LOG.debug("{} is now closed: ", this);
+         closed();
+      }
+      else {
+         remotelyClosed(connection);
+      }
+   }
+
+   @Override
+   public void processRemoteClose(AmqpConnection connection) throws IOException {
+      doClosedInspection();
+      if (isAwaitingClose()) {
+         LOG.debug("{} is now closed: ", this);
+         closed();
+      }
+      else if (isAwaitingOpen()) {
+         // Error on Open, create exception and signal failure.
+         LOG.warn("Open of {} failed: ", this);
+         Exception openError;
+         if (hasRemoteError()) {
+            openError = AmqpSupport.convertToException(getEndpoint().getRemoteCondition());
+         }
+         else {
+            openError = getOpenAbortException();
+         }
+
+         failed(openError);
+      }
+      else {
+         remotelyClosed(connection);
+      }
+   }
+
+   @Override
+   public void processDeliveryUpdates(AmqpConnection connection) throws IOException {
+   }
+
+   @Override
+   public void processFlowUpdates(AmqpConnection connection) throws IOException {
+   }
+
+   /**
+    * Perform the open operation on the managed endpoint.  A subclass may
+    * override this method to provide additional open actions or configuration
+    * updates.
+    */
+   protected void doOpen() {
+      getEndpoint().open();
+   }
+
+   /**
+    * Perform the close operation on the managed endpoint.  A subclass may
+    * override this method to provide additional close actions or alter the
+    * standard close path such as endpoint detach etc.
+    */
+   protected void doClose() {
+      getEndpoint().close();
+   }
+
+   /**
+    * Perform the detach operation on the managed endpoint.
+    *
+    * By default this method throws an UnsupportedOperationException, a subclass
+    * must implement this and do a detach if its resource supports that.
+    */
+   protected void doDetach() {
+      throw new UnsupportedOperationException("Endpoint cannot be detached.");
+   }
+
+   /**
+    * Complete the open operation on the managed endpoint. A subclass may
+    * override this method to provide additional verification actions or configuration
+    * updates.
+    */
+   protected void doOpenCompletion() {
+      LOG.debug("{} is now open: ", this);
+      opened();
+   }
+
+   /**
+    * When aborting the open operation, and there isnt an error condition,
+    * provided by the peer, the returned exception will be used instead.
+    * A subclass may override this method to provide alternative behaviour.
+    */
+   protected Exception getOpenAbortException() {
+      return new IOException("Open failed unexpectedly.");
+   }
+
+   // TODO - Fina a more generic way to do this.
+   protected abstract void doOpenInspection();
+
+   protected abstract void doClosedInspection();
+
+   protected void doDetachedInspection() {
+   }
+
+   //----- Private implementation utility methods ---------------------------//
+
+   private boolean isAwaitingOpen() {
+      return this.openRequest != null;
+   }
+
+   private boolean isAwaitingClose() {
+      return this.closeRequest != null;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpClient.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpClient.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpClient.java
new file mode 100644
index 0000000..001942e
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpClient.java
@@ -0,0 +1,245 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.activemq.transport.amqp.client.transport.NettyTransport;
+import org.apache.activemq.transport.amqp.client.transport.NettyTransportFactory;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Connection instance used to connect to the Broker using Proton as
+ * the AMQP protocol handler.
+ */
+public class AmqpClient {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpClient.class);
+
+   private final String username;
+   private final String password;
+   private final URI remoteURI;
+   private String authzid;
+   private String mechanismRestriction;
+
+   private AmqpValidator stateInspector = new AmqpValidator();
+   private List<Symbol> offeredCapabilities = Collections.emptyList();
+   private Map<Symbol, Object> offeredProperties = Collections.emptyMap();
+
+   /**
+    * Creates an AmqpClient instance which can be used as a factory for connections.
+    *
+    * @param remoteURI The address of the remote peer to connect to.
+    * @param username  The user name to use when authenticating the client.
+    * @param password  The password to use when authenticating the client.
+    */
+   public AmqpClient(URI remoteURI, String username, String password) {
+      this.remoteURI = remoteURI;
+      this.password = password;
+      this.username = username;
+   }
+
+   /**
+    * Creates a connection with the broker at the given location, this method initiates a
+    * connect attempt immediately and will fail if the remote peer cannot be reached.
+    *
+    * @throws Exception if an error occurs attempting to connect to the Broker.
+    * @returns a new connection object used to interact with the connected peer.
+    */
+   public AmqpConnection connect() throws Exception {
+
+      AmqpConnection connection = createConnection();
+
+      LOG.debug("Attempting to create new connection to peer: {}", remoteURI);
+      connection.connect();
+
+      return connection;
+   }
+
+   /**
+    * Creates a connection object using the configured values for user, password, remote URI
+    * etc.  This method does not immediately initiate a connection to the remote leaving that
+    * to the caller which provides a connection object that can have additional configuration
+    * changes applied before the <code>connect</code> method is invoked.
+    *
+    * @throws Exception if an error occurs attempting to connect to the Broker.
+    * @returns a new connection object used to interact with the connected peer.
+    */
+   public AmqpConnection createConnection() throws Exception {
+      if (username == null && password != null) {
+         throw new IllegalArgumentException("Password must be null if user name value is null");
+      }
+
+      NettyTransport transport = NettyTransportFactory.createTransport(remoteURI);
+      AmqpConnection connection = new AmqpConnection(transport, username, password);
+
+      connection.setMechanismRestriction(mechanismRestriction);
+      connection.setAuthzid(authzid);
+
+      connection.setOfferedCapabilities(getOfferedCapabilities());
+      connection.setOfferedProperties(getOfferedProperties());
+      connection.setStateInspector(getStateInspector());
+
+      return connection;
+   }
+
+   /**
+    * @return the user name value given when constructed.
+    */
+   public String getUsername() {
+      return username;
+   }
+
+   /**
+    * @return the password value given when constructed.
+    */
+   public String getPassword() {
+      return password;
+   }
+
+   /**
+    * @param authzid The authzid used when authenticating (currently only with PLAIN)
+    */
+   public void setAuthzid(String authzid) {
+      this.authzid = authzid;
+   }
+
+   public String getAuthzid() {
+      return authzid;
+   }
+
+   /**
+    * @param mechanismRestriction The mechanism to use when authenticating (if offered by the server)
+    */
+   public void setMechanismRestriction(String mechanismRestriction) {
+      this.mechanismRestriction = mechanismRestriction;
+   }
+
+   public String getMechanismRestriction() {
+      return mechanismRestriction;
+   }
+
+   /**
+    * @return the currently set address to use to connect to the AMQP peer.
+    */
+   public URI getRemoteURI() {
+      return remoteURI;
+   }
+
+   /**
+    * Sets the offered capabilities that should be used when a new connection attempt
+    * is made.
+    *
+    * @param offeredCapabilities the list of capabilities to offer when connecting.
+    */
+   public void setOfferedCapabilities(List<Symbol> offeredCapabilities) {
+      if (offeredCapabilities != null) {
+         offeredCapabilities = Collections.emptyList();
+      }
+
+      this.offeredCapabilities = offeredCapabilities;
+   }
+
+   /**
+    * @return an unmodifiable view of the currently set offered capabilities
+    */
+   public List<Symbol> getOfferedCapabilities() {
+      return Collections.unmodifiableList(offeredCapabilities);
+   }
+
+   /**
+    * Sets the offered connection properties that should be used when a new connection
+    * attempt is made.
+    *
+    * @param offeredProperties the map of properties to offer when connecting.
+    */
+   public void setOfferedProperties(Map<Symbol, Object> offeredProperties) {
+      if (offeredProperties != null) {
+         offeredProperties = Collections.emptyMap();
+      }
+
+      this.offeredProperties = offeredProperties;
+   }
+
+   /**
+    * @return an unmodifiable view of the currently set connection properties.
+    */
+   public Map<Symbol, Object> getOfferedProperties() {
+      return Collections.unmodifiableMap(offeredProperties);
+   }
+
+   /**
+    * @return the currently set state inspector used to check state after various events.
+    */
+   public AmqpValidator getStateInspector() {
+      return stateInspector;
+   }
+
+   /**
+    * Sets the state inspector used to check that the AMQP resource is valid after
+    * specific lifecycle events such as open and close.
+    *
+    * @param stateInspector the new state inspector to use.
+    */
+   public void setValidator(AmqpValidator stateInspector) {
+      if (stateInspector == null) {
+         stateInspector = new AmqpValidator();
+      }
+
+      this.stateInspector = stateInspector;
+   }
+
+   @Override
+   public String toString() {
+      return "AmqpClient: " + getRemoteURI().getHost() + ":" + getRemoteURI().getPort();
+   }
+
+   /**
+    * Creates an anonymous connection with the broker at the given location.
+    *
+    * @param broker the address of the remote broker instance.
+    * @throws Exception if an error occurs attempting to connect to the Broker.
+    * @returns a new connection object used to interact with the connected peer.
+    */
+   public static AmqpConnection connect(URI broker) throws Exception {
+      return connect(broker, null, null);
+   }
+
+   /**
+    * Creates a connection with the broker at the given location.
+    *
+    * @param broker   the address of the remote broker instance.
+    * @param username the user name to use to connect to the broker or null for anonymous.
+    * @param password the password to use to connect to the broker, must be null if user name is null.
+    * @throws Exception if an error occurs attempting to connect to the Broker.
+    * @returns a new connection object used to interact with the connected peer.
+    */
+   public static AmqpConnection connect(URI broker, String username, String password) throws Exception {
+      if (username == null && password != null) {
+         throw new IllegalArgumentException("Password must be null if user name value is null");
+      }
+
+      AmqpClient client = new AmqpClient(broker, username, password);
+
+      return client.connect();
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnection.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnection.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnection.java
new file mode 100644
index 0000000..1454dd9
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnection.java
@@ -0,0 +1,720 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.util.ReferenceCountUtil;
+import org.apache.activemq.transport.InactivityIOException;
+import org.apache.activemq.transport.amqp.client.sasl.SaslAuthenticator;
+import org.apache.activemq.transport.amqp.client.transport.NettyTransportListener;
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.activemq.transport.amqp.client.util.ClientFuture;
+import org.apache.activemq.transport.amqp.client.util.IdGenerator;
+import org.apache.activemq.transport.amqp.client.util.NoOpAsyncResult;
+import org.apache.activemq.transport.amqp.client.util.UnmodifiableConnection;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.engine.Collector;
+import org.apache.qpid.proton.engine.Connection;
+import org.apache.qpid.proton.engine.EndpointState;
+import org.apache.qpid.proton.engine.Event;
+import org.apache.qpid.proton.engine.Event.Type;
+import org.apache.qpid.proton.engine.Sasl;
+import org.apache.qpid.proton.engine.Transport;
+import org.apache.qpid.proton.engine.impl.CollectorImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.activemq.transport.amqp.AmqpSupport.CONNECTION_OPEN_FAILED;
+
+public class AmqpConnection extends AmqpAbstractResource<Connection> implements NettyTransportListener {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpConnection.class);
+
+   private static final NoOpAsyncResult NOOP_REQUEST = new NoOpAsyncResult();
+
+   private static final int DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 1;
+   // NOTE: Limit default channel max to signed short range to deal with
+   //       brokers that don't currently handle the unsigned range well.
+   private static final int DEFAULT_CHANNEL_MAX = 32767;
+   private static final IdGenerator CONNECTION_ID_GENERATOR = new IdGenerator();
+
+   public static final long DEFAULT_CONNECT_TIMEOUT = 515000;
+   public static final long DEFAULT_CLOSE_TIMEOUT = 30000;
+   public static final long DEFAULT_DRAIN_TIMEOUT = 60000;
+
+   private final ScheduledExecutorService serializer;
+   private final AtomicBoolean closed = new AtomicBoolean();
+   private final AtomicBoolean connected = new AtomicBoolean();
+   private final AtomicLong sessionIdGenerator = new AtomicLong();
+   private final AtomicLong txIdGenerator = new AtomicLong();
+   private final Collector protonCollector = new CollectorImpl();
+   private final org.apache.activemq.transport.amqp.client.transport.NettyTransport transport;
+   private final Transport protonTransport = Transport.Factory.create();
+
+   private final String username;
+   private final String password;
+   private final URI remoteURI;
+   private final String connectionId;
+   private List<Symbol> offeredCapabilities = Collections.emptyList();
+   private Map<Symbol, Object> offeredProperties = Collections.emptyMap();
+
+   private AmqpConnectionListener listener;
+   private SaslAuthenticator authenticator;
+   private String mechanismRestriction;
+   private String authzid;
+
+   private int idleTimeout = 0;
+   private boolean idleProcessingDisabled;
+   private String containerId;
+   private boolean authenticated;
+   private int channelMax = DEFAULT_CHANNEL_MAX;
+   private long connectTimeout = DEFAULT_CONNECT_TIMEOUT;
+   private long closeTimeout = DEFAULT_CLOSE_TIMEOUT;
+   private long drainTimeout = DEFAULT_DRAIN_TIMEOUT;
+
+   public AmqpConnection(org.apache.activemq.transport.amqp.client.transport.NettyTransport transport,
+                         String username,
+                         String password) {
+      setEndpoint(Connection.Factory.create());
+      getEndpoint().collect(protonCollector);
+
+      this.transport = transport;
+      this.username = username;
+      this.password = password;
+      this.connectionId = CONNECTION_ID_GENERATOR.generateId();
+      this.remoteURI = transport.getRemoteLocation();
+
+      this.serializer = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+
+         @Override
+         public Thread newThread(Runnable runner) {
+            Thread serial = new Thread(runner);
+            serial.setDaemon(true);
+            serial.setName(toString());
+            return serial;
+         }
+      });
+
+      this.transport.setTransportListener(this);
+   }
+
+   public void connect() throws Exception {
+      if (connected.compareAndSet(false, true)) {
+         transport.connect();
+
+         final ClientFuture future = new ClientFuture();
+         serializer.execute(new Runnable() {
+            @Override
+            public void run() {
+               getEndpoint().setContainer(safeGetContainerId());
+               getEndpoint().setHostname(remoteURI.getHost());
+               if (!getOfferedCapabilities().isEmpty()) {
+                  getEndpoint().setOfferedCapabilities(getOfferedCapabilities().toArray(new Symbol[0]));
+               }
+               if (!getOfferedProperties().isEmpty()) {
+                  getEndpoint().setProperties(getOfferedProperties());
+               }
+
+               if (getIdleTimeout() > 0) {
+                  protonTransport.setIdleTimeout(getIdleTimeout());
+               }
+               protonTransport.setMaxFrameSize(getMaxFrameSize());
+               protonTransport.setChannelMax(getChannelMax());
+               protonTransport.bind(getEndpoint());
+               Sasl sasl = protonTransport.sasl();
+               if (sasl != null) {
+                  sasl.client();
+               }
+               authenticator = new SaslAuthenticator(sasl, username, password, authzid, mechanismRestriction);
+               open(future);
+
+               pumpToProtonTransport(future);
+            }
+         });
+
+         if (connectTimeout <= 0) {
+            future.sync();
+         }
+         else {
+            future.sync(connectTimeout, TimeUnit.MILLISECONDS);
+            if (getEndpoint().getRemoteState() != EndpointState.ACTIVE) {
+               throw new IOException("Failed to connect after configured timeout.");
+            }
+         }
+      }
+   }
+
+   public boolean isConnected() {
+      return transport.isConnected() && connected.get();
+   }
+
+   public void close() {
+      if (closed.compareAndSet(false, true)) {
+         final ClientFuture request = new ClientFuture();
+         serializer.execute(new Runnable() {
+
+            @Override
+            public void run() {
+               try {
+
+                  // If we are not connected then there is nothing we can do now
+                  // just signal success.
+                  if (!transport.isConnected()) {
+                     request.onSuccess();
+                  }
+
+                  if (getEndpoint() != null) {
+                     close(request);
+                  }
+                  else {
+                     request.onSuccess();
+                  }
+
+                  pumpToProtonTransport(request);
+               }
+               catch (Exception e) {
+                  LOG.debug("Caught exception while closing proton connection");
+               }
+            }
+         });
+
+         try {
+            if (closeTimeout <= 0) {
+               request.sync();
+            }
+            else {
+               request.sync(closeTimeout, TimeUnit.MILLISECONDS);
+            }
+         }
+         catch (IOException e) {
+            LOG.warn("Error caught while closing Provider: ", e.getMessage());
+         }
+         finally {
+            if (transport != null) {
+               try {
+                  transport.close();
+               }
+               catch (Exception e) {
+                  LOG.debug("Cuaght exception while closing down Transport: {}", e.getMessage());
+               }
+            }
+
+            serializer.shutdown();
+         }
+      }
+   }
+
+   /**
+    * Creates a new Session instance used to create AMQP resources like
+    * senders and receivers.
+    *
+    * @return a new AmqpSession that can be used to create links.
+    * @throws Exception if an error occurs during creation.
+    */
+   public AmqpSession createSession() throws Exception {
+      checkClosed();
+
+      final AmqpSession session = new AmqpSession(AmqpConnection.this, getNextSessionId());
+      final ClientFuture request = new ClientFuture();
+
+      serializer.execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            session.setEndpoint(getEndpoint().session());
+            session.setStateInspector(getStateInspector());
+            session.open(request);
+            pumpToProtonTransport(request);
+         }
+      });
+
+      request.sync();
+
+      return session;
+   }
+
+   //----- Access to low level IO for specific test cases -------------------//
+
+   public void sendRawBytes(final byte[] rawData) throws Exception {
+      checkClosed();
+
+      final ClientFuture request = new ClientFuture();
+
+      serializer.execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               transport.send(Unpooled.wrappedBuffer(rawData));
+            }
+            catch (IOException e) {
+               fireClientException(e);
+            }
+            finally {
+               request.onSuccess();
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   //----- Configuration accessors ------------------------------------------//
+
+   /**
+    * @return the user name that was used to authenticate this connection.
+    */
+   public String getUsername() {
+      return username;
+   }
+
+   /**
+    * @return the password that was used to authenticate this connection.
+    */
+   public String getPassword() {
+      return password;
+   }
+
+   public void setAuthzid(String authzid) {
+      this.authzid = authzid;
+   }
+
+   public String getAuthzid() {
+      return authzid;
+   }
+
+   /**
+    * @return the URI of the remote peer this connection attached to.
+    */
+   public URI getRemoteURI() {
+      return remoteURI;
+   }
+
+   /**
+    * @return the container ID that will be set as the container Id.
+    */
+   public String getContainerId() {
+      return this.containerId;
+   }
+
+   /**
+    * Sets the container Id that will be configured on the connection prior to
+    * connecting to the remote peer.  Calling this after connect has no effect.
+    *
+    * @param containerId the container Id to use on the connection.
+    */
+   public void setContainerId(String containerId) {
+      this.containerId = containerId;
+   }
+
+   /**
+    * @return the currently set Max Frame Size value.
+    */
+   public int getMaxFrameSize() {
+      return DEFAULT_MAX_FRAME_SIZE;
+   }
+
+   public int getChannelMax() {
+      return channelMax;
+   }
+
+   public void setChannelMax(int channelMax) {
+      this.channelMax = channelMax;
+   }
+
+   public long getConnectTimeout() {
+      return connectTimeout;
+   }
+
+   public void setConnectTimeout(long connectTimeout) {
+      this.connectTimeout = connectTimeout;
+   }
+
+   public long getCloseTimeout() {
+      return closeTimeout;
+   }
+
+   public void setCloseTimeout(long closeTimeout) {
+      this.closeTimeout = closeTimeout;
+   }
+
+   public long getDrainTimeout() {
+      return drainTimeout;
+   }
+
+   public void setDrainTimeout(long drainTimeout) {
+      this.drainTimeout = drainTimeout;
+   }
+
+   public List<Symbol> getOfferedCapabilities() {
+      return offeredCapabilities;
+   }
+
+   public void setOfferedCapabilities(List<Symbol> offeredCapabilities) {
+      if (offeredCapabilities != null) {
+         offeredCapabilities = Collections.emptyList();
+      }
+
+      this.offeredCapabilities = offeredCapabilities;
+   }
+
+   public Map<Symbol, Object> getOfferedProperties() {
+      return offeredProperties;
+   }
+
+   public void setOfferedProperties(Map<Symbol, Object> offeredProperties) {
+      if (offeredProperties != null) {
+         offeredProperties = Collections.emptyMap();
+      }
+
+      this.offeredProperties = offeredProperties;
+   }
+
+   public Connection getConnection() {
+      return new UnmodifiableConnection(getEndpoint());
+   }
+
+   public AmqpConnectionListener getListener() {
+      return listener;
+   }
+
+   public void setListener(AmqpConnectionListener listener) {
+      this.listener = listener;
+   }
+
+   public int getIdleTimeout() {
+      return idleTimeout;
+   }
+
+   public void setIdleTimeout(int idleTimeout) {
+      this.idleTimeout = idleTimeout;
+   }
+
+   public void setIdleProcessingDisabled(boolean value) {
+      this.idleProcessingDisabled = value;
+   }
+
+   public boolean isIdleProcessingDisabled() {
+      return idleProcessingDisabled;
+   }
+
+   /**
+    * Sets a restriction on the SASL mechanism to use (if offered by the server).
+    *
+    * @param mechanismRestriction the mechanism to use
+    */
+   public void setMechanismRestriction(String mechanismRestriction) {
+      this.mechanismRestriction = mechanismRestriction;
+   }
+
+   public String getMechanismRestriction() {
+      return mechanismRestriction;
+   }
+
+   //----- Internal getters used from the child AmqpResource classes --------//
+
+   ScheduledExecutorService getScheduler() {
+      return this.serializer;
+   }
+
+   Connection getProtonConnection() {
+      return getEndpoint();
+   }
+
+   String getConnectionId() {
+      return this.connectionId;
+   }
+
+   AmqpTransactionId getNextTransactionId() {
+      return new AmqpTransactionId(connectionId + ":" + txIdGenerator.incrementAndGet());
+   }
+
+   void pumpToProtonTransport() {
+      pumpToProtonTransport(NOOP_REQUEST);
+   }
+
+   void pumpToProtonTransport(AsyncResult request) {
+      try {
+         boolean done = false;
+         while (!done) {
+            ByteBuffer toWrite = protonTransport.getOutputBuffer();
+            if (toWrite != null && toWrite.hasRemaining()) {
+               ByteBuf outbound = transport.allocateSendBuffer(toWrite.remaining());
+               outbound.writeBytes(toWrite);
+               transport.send(outbound);
+               protonTransport.outputConsumed();
+            }
+            else {
+               done = true;
+            }
+         }
+      }
+      catch (IOException e) {
+         fireClientException(e);
+         request.onFailure(e);
+      }
+   }
+
+   //----- Transport listener event hooks -----------------------------------//
+
+   @Override
+   public void onData(final ByteBuf incoming) {
+
+      // We need to retain until the serializer gets around to processing it.
+      ReferenceCountUtil.retain(incoming);
+
+      serializer.execute(new Runnable() {
+
+         @Override
+         public void run() {
+            ByteBuffer source = incoming.nioBuffer();
+            LOG.trace("Client Received from Broker {} bytes:", source.remaining());
+
+            if (protonTransport.isClosed()) {
+               LOG.debug("Ignoring incoming data because transport is closed");
+               return;
+            }
+
+            do {
+               ByteBuffer buffer = protonTransport.getInputBuffer();
+               int limit = Math.min(buffer.remaining(), source.remaining());
+               ByteBuffer duplicate = source.duplicate();
+               duplicate.limit(source.position() + limit);
+               buffer.put(duplicate);
+               protonTransport.processInput();
+               source.position(source.position() + limit);
+            } while (source.hasRemaining());
+
+            ReferenceCountUtil.release(incoming);
+
+            // Process the state changes from the latest data and then answer back
+            // any pending updates to the Broker.
+            processUpdates();
+            pumpToProtonTransport();
+         }
+      });
+   }
+
+   @Override
+   public void onTransportClosed() {
+      LOG.debug("The transport has unexpectedly closed");
+      failed(getOpenAbortException());
+   }
+
+   @Override
+   public void onTransportError(Throwable cause) {
+      fireClientException(cause);
+   }
+
+   //----- Internal implementation ------------------------------------------//
+
+   @Override
+   protected void doOpenCompletion() {
+      // If the remote indicates that a close is pending, don't open.
+      if (getEndpoint().getRemoteProperties() == null || !getEndpoint().getRemoteProperties().containsKey(CONNECTION_OPEN_FAILED)) {
+
+         if (!isIdleProcessingDisabled()) {
+            // Using nano time since it is not related to the wall clock, which may change
+            long initialNow = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+            long initialKeepAliveDeadline = protonTransport.tick(initialNow);
+            if (initialKeepAliveDeadline > 0) {
+
+               getScheduler().schedule(new Runnable() {
+
+                  @Override
+                  public void run() {
+                     try {
+                        if (getEndpoint().getLocalState() != EndpointState.CLOSED) {
+                           LOG.debug("Client performing next idle check");
+                           // Using nano time since it is not related to the wall clock, which may change
+                           long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+                           long rescheduleAt = protonTransport.tick(now) - now;
+                           pumpToProtonTransport();
+                           if (protonTransport.isClosed()) {
+                              LOG.debug("Transport closed after inactivity check.");
+                              throw new InactivityIOException("Channel was inactive for to long");
+                           }
+
+                           if (rescheduleAt > 0) {
+                              getScheduler().schedule(this, rescheduleAt, TimeUnit.MILLISECONDS);
+                           }
+                        }
+                     }
+                     catch (Exception e) {
+                        try {
+                           transport.close();
+                        }
+                        catch (IOException e1) {
+                        }
+                        fireClientException(e);
+                     }
+                  }
+               }, initialKeepAliveDeadline - initialNow, TimeUnit.MILLISECONDS);
+            }
+         }
+         super.doOpenCompletion();
+      }
+   }
+
+   @Override
+   protected void doOpenInspection() {
+      try {
+         getStateInspector().inspectOpenedResource(getConnection());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected void doClosedInspection() {
+      try {
+         getStateInspector().inspectClosedResource(getConnection());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   protected void fireClientException(Throwable ex) {
+      AmqpConnectionListener listener = this.listener;
+      if (listener != null) {
+         listener.onException(ex);
+      }
+   }
+
+   protected void checkClosed() throws IllegalStateException {
+      if (closed.get()) {
+         throw new IllegalStateException("The Connection is already closed");
+      }
+   }
+
+   private void processUpdates() {
+      try {
+         Event protonEvent = null;
+         while ((protonEvent = protonCollector.peek()) != null) {
+            if (!protonEvent.getType().equals(Type.TRANSPORT)) {
+               LOG.trace("Client: New Proton Event: {}", protonEvent.getType());
+            }
+
+            AmqpEventSink amqpEventSink = null;
+            switch (protonEvent.getType()) {
+               case CONNECTION_REMOTE_CLOSE:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getConnection().getContext();
+                  amqpEventSink.processRemoteClose(this);
+                  break;
+               case CONNECTION_REMOTE_OPEN:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getConnection().getContext();
+                  amqpEventSink.processRemoteOpen(this);
+                  break;
+               case SESSION_REMOTE_CLOSE:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getSession().getContext();
+                  amqpEventSink.processRemoteClose(this);
+                  break;
+               case SESSION_REMOTE_OPEN:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getSession().getContext();
+                  amqpEventSink.processRemoteOpen(this);
+                  break;
+               case LINK_REMOTE_CLOSE:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext();
+                  amqpEventSink.processRemoteClose(this);
+                  break;
+               case LINK_REMOTE_DETACH:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext();
+                  amqpEventSink.processRemoteDetach(this);
+                  break;
+               case LINK_REMOTE_OPEN:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext();
+                  amqpEventSink.processRemoteOpen(this);
+                  break;
+               case LINK_FLOW:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext();
+                  amqpEventSink.processFlowUpdates(this);
+                  break;
+               case DELIVERY:
+                  amqpEventSink = (AmqpEventSink) protonEvent.getLink().getContext();
+                  amqpEventSink.processDeliveryUpdates(this);
+                  break;
+               default:
+                  break;
+            }
+
+            protonCollector.pop();
+         }
+
+         // We have to do this to pump SASL bytes in as SASL is not event driven yet.
+         if (!authenticated) {
+            processSaslAuthentication();
+         }
+      }
+      catch (Exception ex) {
+         LOG.warn("Caught Exception during update processing: {}", ex.getMessage(), ex);
+         fireClientException(ex);
+      }
+   }
+
+   private void processSaslAuthentication() {
+      if (authenticated || authenticator == null) {
+         return;
+      }
+
+      try {
+         if (authenticator.authenticate()) {
+            authenticator = null;
+            authenticated = true;
+         }
+      }
+      catch (SecurityException ex) {
+         failed(ex);
+      }
+   }
+
+   private String getNextSessionId() {
+      return connectionId + ":" + sessionIdGenerator.incrementAndGet();
+   }
+
+   private String safeGetContainerId() {
+      String containerId = getContainerId();
+      if (containerId == null || containerId.isEmpty()) {
+         containerId = UUID.randomUUID().toString();
+      }
+
+      return containerId;
+   }
+
+   @Override
+   public String toString() {
+      return "AmqpConnection { " + connectionId + " }";
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnectionListener.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnectionListener.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnectionListener.java
new file mode 100644
index 0000000..822170a
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpConnectionListener.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.activemq.transport.amqp.client;
+
+/**
+ * Events points exposed by the AmqpClient object.
+ */
+public interface AmqpConnectionListener {
+
+   /**
+    * Indicates some error has occurred during client operations.
+    *
+    * @param ex The error that triggered this event.
+    */
+   void onException(Throwable ex);
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpDefaultConnectionListener.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpDefaultConnectionListener.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpDefaultConnectionListener.java
new file mode 100644
index 0000000..d2492e9
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpDefaultConnectionListener.java
@@ -0,0 +1,28 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+/**
+ * Default listener implementation that stubs out all the event methods.
+ */
+public class AmqpDefaultConnectionListener implements AmqpConnectionListener {
+
+   @Override
+   public void onException(Throwable ex) {
+
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpEventSink.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpEventSink.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpEventSink.java
new file mode 100644
index 0000000..1c511a5
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpEventSink.java
@@ -0,0 +1,69 @@
+/*
+ * 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.activemq.transport.amqp.client;
+
+import java.io.IOException;
+
+/**
+ * Interface used by classes that want to process AMQP events sent from
+ * the transport layer.
+ */
+public interface AmqpEventSink {
+
+   /**
+    * Event handler for remote peer open of this resource.
+    *
+    * @param connection the AmqpConnection instance for easier access to fire events.
+    * @throws IOException if an error occurs while processing the update.
+    */
+   void processRemoteOpen(AmqpConnection connection) throws IOException;
+
+   /**
+    * Event handler for remote peer detach of this resource.
+    *
+    * @param connection the AmqpConnection instance for easier access to fire events.
+    * @throws IOException if an error occurs while processing the update.
+    */
+   void processRemoteDetach(AmqpConnection connection) throws IOException;
+
+   /**
+    * Event handler for remote peer close of this resource.
+    *
+    * @param connection the AmqpConnection instance for easier access to fire events.
+    * @throws IOException if an error occurs while processing the update.
+    */
+   void processRemoteClose(AmqpConnection connection) throws IOException;
+
+   /**
+    * Called when the Proton Engine signals an Delivery related event has been triggered
+    * for the given endpoint.
+    *
+    * @param connection the AmqpConnection instance for easier access to fire events.
+    * @throws IOException if an error occurs while processing the update.
+    */
+   void processDeliveryUpdates(AmqpConnection connection) throws IOException;
+
+   /**
+    * Called when the Proton Engine signals an Flow related event has been triggered
+    * for the given endpoint.
+    *
+    * @param connection the AmqpConnection instance for easier access to fire events.
+    * @throws IOException if an error occurs while processing the update.
+    */
+   void processFlowUpdates(AmqpConnection connection) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpJmsSelectorFilter.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpJmsSelectorFilter.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpJmsSelectorFilter.java
new file mode 100644
index 0000000..adf5df6
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpJmsSelectorFilter.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.activemq.transport.amqp.client;
+
+import org.apache.qpid.proton.amqp.DescribedType;
+
+import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_CODE;
+
+/**
+ * A Described Type wrapper for JMS selector values.
+ */
+public class AmqpJmsSelectorFilter implements DescribedType {
+
+   private final String selector;
+
+   public AmqpJmsSelectorFilter(String selector) {
+      this.selector = selector;
+   }
+
+   @Override
+   public Object getDescriptor() {
+      return JMS_SELECTOR_CODE;
+   }
+
+   @Override
+   public Object getDescribed() {
+      return this.selector;
+   }
+
+   @Override
+   public String toString() {
+      return "AmqpJmsSelectorType{" + selector + "}";
+   }
+}


[3/9] activemq-artemis git commit: ARTEMIS-627 document details of Producer BLOCK in CORE

Posted by an...@apache.org.
ARTEMIS-627 document details of Producer BLOCK in CORE


Project: http://git-wip-us.apache.org/repos/asf/activemq-artemis/repo
Commit: http://git-wip-us.apache.org/repos/asf/activemq-artemis/commit/5695164b
Tree: http://git-wip-us.apache.org/repos/asf/activemq-artemis/tree/5695164b
Diff: http://git-wip-us.apache.org/repos/asf/activemq-artemis/diff/5695164b

Branch: refs/heads/master
Commit: 5695164b873d13c0909def0f0cf4569f6128b88c
Parents: fe27cd8
Author: Martyn Taylor <mt...@redhat.com>
Authored: Mon Jul 18 14:51:56 2016 +0100
Committer: Andy Taylor <an...@gmail.com>
Committed: Wed Jul 20 10:33:44 2016 +0100

----------------------------------------------------------------------
 docs/user-manual/en/flow-control.md | 26 +++++++++++++++++++++-----
 1 file changed, 21 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/5695164b/docs/user-manual/en/flow-control.md
----------------------------------------------------------------------
diff --git a/docs/user-manual/en/flow-control.md b/docs/user-manual/en/flow-control.md
index 48d426e..054bcce 100644
--- a/docs/user-manual/en/flow-control.md
+++ b/docs/user-manual/en/flow-control.md
@@ -198,12 +198,19 @@ size can be set via the
 `ActiveMQConnectionFactory.setProducerWindowSize(int
                   producerWindowSize)` method.
 
-#### Blocking producer window based flow control
+#### Blocking producer window based flow control using CORE protocol
 
-Normally the server will always give the same number of credits as have
-been requested. However, it is also possible to set a maximum size on
-any address, and the server will never send more credits than could
-cause the address's upper memory limit to be exceeded.
+When using the CORE protocol (used by both the Artemis Core Client and Artemis JMS Client)
+the server will always aim give the same number of credits as have been requested.
+However, it is also possible to set a maximum size on any address, and the server
+will never send more credits to any one producer than what is available according to
+the address's upper memory limit.  Although a single producer will be issued more
+credits than available (at the time of issue) it is possible that more than 1
+producer be associated with the same address and so it is theoretically possible
+that more credits are allocated across total producers than what is available.
+It is therefore possible to go over the address limit by approximately:
+
+ '''total number of producers on address * producer window size'''
 
 For example, if I have a JMS queue called "myqueue", I could set the
 maximum memory size to 10MiB, and the the server will control the number
@@ -257,6 +264,15 @@ control.
 > want this behaviour increase the `max-size-bytes` parameter or change
 > the address full message policy.
 
+> **Note**
+>
+> Producer credits are allocated from the broker to the client.  Flow control
+> credit checking (i.e. checking a producer has enough credit) is done on the
+> client side only.  It is possible for the broker to over allocate credits, like
+> in the multiple producer scenario outlined above.  It is also possible for
+> a misbehaving client to ignore the flow control credits issued by the broker
+> and continue sending with out sufficient credit.
+
 ### Rate limited flow control
 
 Apache ActiveMQ Artemis also allows the rate a producer can emit message to be limited,


[4/9] activemq-artemis git commit: ARTEMIS-636 Implement AMQP AddressFull BLOCK

Posted by an...@apache.org.
ARTEMIS-636 Implement AMQP AddressFull BLOCK


Project: http://git-wip-us.apache.org/repos/asf/activemq-artemis/repo
Commit: http://git-wip-us.apache.org/repos/asf/activemq-artemis/commit/4d60ced5
Tree: http://git-wip-us.apache.org/repos/asf/activemq-artemis/tree/4d60ced5
Diff: http://git-wip-us.apache.org/repos/asf/activemq-artemis/diff/4d60ced5

Branch: refs/heads/master
Commit: 4d60ced581f28d9ffcd8ab4cef9130bf07715209
Parents: 5dfa1c5
Author: Martyn Taylor <mt...@redhat.com>
Authored: Mon Jul 18 14:08:41 2016 +0100
Committer: Andy Taylor <an...@gmail.com>
Committed: Wed Jul 20 10:33:44 2016 +0100

----------------------------------------------------------------------
 .../plug/ProtonSessionIntegrationCallback.java  |  64 +++++--
 .../org/proton/plug/AMQPSessionCallback.java    |   2 +
 .../plug/context/AbstractConnectionContext.java |   2 +-
 .../context/AbstractProtonReceiverContext.java  |   5 +-
 .../client/ProtonClientReceiverContext.java     |   5 +
 .../server/ProtonServerReceiverContext.java     |  21 ++-
 .../server/ProtonServerSenderContext.java       |   4 +-
 .../test/minimalserver/MinimalSessionSPI.java   |   7 +-
 docs/user-manual/en/flow-control.md             |  22 +++
 .../tests/integration/proton/ProtonTest.java    | 185 ++++++++++++++++++-
 10 files changed, 283 insertions(+), 34 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/proton/plug/ProtonSessionIntegrationCallback.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/proton/plug/ProtonSessionIntegrationCallback.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/proton/plug/ProtonSessionIntegrationCallback.java
index 00f5e3f..ab57fe1 100644
--- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/proton/plug/ProtonSessionIntegrationCallback.java
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/core/protocol/proton/plug/ProtonSessionIntegrationCallback.java
@@ -20,33 +20,37 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import io.netty.buffer.ByteBuf;
+import org.apache.activemq.artemis.api.core.SimpleString;
+import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
 import org.apache.activemq.artemis.core.io.IOCallback;
+import org.apache.activemq.artemis.core.paging.PagingStore;
+import org.apache.activemq.artemis.core.protocol.proton.ProtonProtocolManager;
 import org.apache.activemq.artemis.core.protocol.proton.converter.message.EncodedMessage;
 import org.apache.activemq.artemis.core.server.MessageReference;
+import org.apache.activemq.artemis.core.server.QueueQueryResult;
+import org.apache.activemq.artemis.core.server.ServerConsumer;
+import org.apache.activemq.artemis.core.server.ServerMessage;
+import org.apache.activemq.artemis.core.server.ServerSession;
 import org.apache.activemq.artemis.core.server.impl.ServerConsumerImpl;
+import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy;
 import org.apache.activemq.artemis.core.transaction.Transaction;
+import org.apache.activemq.artemis.spi.core.protocol.SessionCallback;
 import org.apache.activemq.artemis.spi.core.remoting.Connection;
 import org.apache.activemq.artemis.spi.core.remoting.ReadyListener;
+import org.apache.activemq.artemis.utils.ByteUtil;
+import org.apache.activemq.artemis.utils.IDGenerator;
 import org.apache.activemq.artemis.utils.SelectorTranslator;
+import org.apache.activemq.artemis.utils.SimpleIDGenerator;
+import org.apache.activemq.artemis.utils.UUIDGenerator;
 import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.messaging.Accepted;
+import org.apache.qpid.proton.amqp.messaging.Rejected;
 import org.apache.qpid.proton.amqp.transport.AmqpError;
 import org.apache.qpid.proton.amqp.transport.ErrorCondition;
 import org.apache.qpid.proton.engine.Delivery;
 import org.apache.qpid.proton.engine.Link;
 import org.apache.qpid.proton.engine.Receiver;
 import org.apache.qpid.proton.message.ProtonJMessage;
-import org.apache.activemq.artemis.api.core.SimpleString;
-import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
-import org.apache.activemq.artemis.core.protocol.proton.ProtonProtocolManager;
-import org.apache.activemq.artemis.core.server.QueueQueryResult;
-import org.apache.activemq.artemis.core.server.ServerConsumer;
-import org.apache.activemq.artemis.core.server.ServerMessage;
-import org.apache.activemq.artemis.core.server.ServerSession;
-import org.apache.activemq.artemis.spi.core.protocol.SessionCallback;
-import org.apache.activemq.artemis.utils.ByteUtil;
-import org.apache.activemq.artemis.utils.IDGenerator;
-import org.apache.activemq.artemis.utils.SimpleIDGenerator;
-import org.apache.activemq.artemis.utils.UUIDGenerator;
 import org.proton.plug.AMQPConnectionContext;
 import org.proton.plug.AMQPSessionCallback;
 import org.proton.plug.AMQPSessionContext;
@@ -66,7 +70,6 @@ public class ProtonSessionIntegrationCallback implements AMQPSessionCallback, Se
 
    private final Connection transportConnection;
 
-
    private ServerSession serverSession;
 
    private AMQPSessionContext protonSession;
@@ -347,13 +350,28 @@ public class ProtonSessionIntegrationCallback implements AMQPSessionCallback, Se
 
       recoverContext();
 
+      PagingStore store = manager.getServer().getPagingManager().getPageStore(message.getAddress());
+      if (store.isFull() && store.getAddressFullMessagePolicy() == AddressFullMessagePolicy.BLOCK) {
+         ErrorCondition ec = new ErrorCondition(AmqpError.RESOURCE_LIMIT_EXCEEDED, "Address is full: " + message.getAddress());
+         Rejected rejected = new Rejected();
+         rejected.setError(ec);
+         delivery.disposition(rejected);
+         connection.flush();
+      }
+      else {
+         serverSend(message, delivery, receiver);
+      }
+   }
+
+   private void serverSend(final ServerMessage message, final Delivery delivery, final Receiver receiver) throws Exception {
       try {
          serverSession.send(message, false);
-
+         // FIXME Potential race here...
          manager.getServer().getStorageManager().afterCompleteOperations(new IOCallback() {
             @Override
             public void done() {
                synchronized (connection.getLock()) {
+                  delivery.disposition(Accepted.getInstance());
                   delivery.settle();
                   connection.flush();
                }
@@ -379,6 +397,24 @@ public class ProtonSessionIntegrationCallback implements AMQPSessionCallback, Se
    }
 
    @Override
+   public void offerProducerCredit(final String address, final int credits, final int threshold, final Receiver receiver) {
+      try {
+         final PagingStore store = manager.getServer().getPagingManager().getPageStore(new SimpleString(address));
+         store.checkMemory(new Runnable() {
+            @Override
+            public void run() {
+               if (receiver.getRemoteCredit() < threshold) {
+                  receiver.flow(credits);
+               }
+            }
+         });
+      }
+      catch (Exception e) {
+         throw new RuntimeException(e);
+      }
+   }
+
+   @Override
    public void deleteQueue(String address) throws Exception {
       manager.getServer().destroyQueue(new SimpleString(address));
    }

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/AMQPSessionCallback.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/AMQPSessionCallback.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/AMQPSessionCallback.java
index bb53791..637b538 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/AMQPSessionCallback.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/AMQPSessionCallback.java
@@ -44,6 +44,8 @@ public interface AMQPSessionCallback {
 
    void createDurableQueue(String address, String queueName) throws Exception;
 
+   void offerProducerCredit(String address, int credits, int threshold, Receiver receiver);
+
    void deleteQueue(String address) throws Exception;
 
    boolean queueQuery(String queueName) throws Exception;

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractConnectionContext.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractConnectionContext.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractConnectionContext.java
index d6269e8..fa949d3 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractConnectionContext.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractConnectionContext.java
@@ -39,8 +39,8 @@ import org.proton.plug.handler.ProtonHandler;
 import org.proton.plug.handler.impl.DefaultEventHandler;
 import org.proton.plug.util.ByteUtil;
 
-import static org.proton.plug.context.AMQPConstants.Connection.DEFAULT_IDLE_TIMEOUT;
 import static org.proton.plug.context.AMQPConstants.Connection.DEFAULT_CHANNEL_MAX;
+import static org.proton.plug.context.AMQPConstants.Connection.DEFAULT_IDLE_TIMEOUT;
 import static org.proton.plug.context.AMQPConstants.Connection.DEFAULT_MAX_FRAME_SIZE;
 
 public abstract class AbstractConnectionContext extends ProtonInitializable implements AMQPConnectionContext {

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractProtonReceiverContext.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractProtonReceiverContext.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractProtonReceiverContext.java
index 4343b01..5a43029 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractProtonReceiverContext.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/AbstractProtonReceiverContext.java
@@ -57,14 +57,13 @@ public abstract class AbstractProtonReceiverContext extends ProtonInitializable
       close(false);
    }
 
-   public void flow(int credits) {
+   public void flow(int credits, int threshold) {
       synchronized (connection.getLock()) {
-         receiver.flow(credits);
+         sessionSPI.offerProducerCredit(address, credits, threshold, receiver);
       }
       connection.flush();
    }
 
-
    public void drain(int credits) {
       synchronized (connection.getLock()) {
          receiver.drain(credits);

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/client/ProtonClientReceiverContext.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/client/ProtonClientReceiverContext.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/client/ProtonClientReceiverContext.java
index 884af60..c06ae58 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/client/ProtonClientReceiverContext.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/client/ProtonClientReceiverContext.java
@@ -84,4 +84,9 @@ public class ProtonClientReceiverContext extends AbstractProtonReceiverContext i
       return queues.poll(time, unit);
    }
 
+   @Override
+   public void flow(int credits) {
+      flow(credits, Integer.MAX_VALUE);
+   }
+
 }

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerReceiverContext.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerReceiverContext.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerReceiverContext.java
index aa04cef..7d39bb7 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerReceiverContext.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerReceiverContext.java
@@ -19,7 +19,6 @@ package org.proton.plug.context.server;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.PooledByteBufAllocator;
 import org.apache.qpid.proton.amqp.Symbol;
-import org.apache.qpid.proton.amqp.messaging.Accepted;
 import org.apache.qpid.proton.amqp.messaging.Rejected;
 import org.apache.qpid.proton.amqp.transport.ErrorCondition;
 import org.apache.qpid.proton.engine.Delivery;
@@ -39,7 +38,14 @@ public class ProtonServerReceiverContext extends AbstractProtonReceiverContext {
 
    private static final Logger log = Logger.getLogger(ProtonServerReceiverContext.class);
 
-   private final int numberOfCredits = 100;
+   /*
+    The maximum number of credits we will allocate to clients.
+    This number is also used by the broker when refresh client credits.
+     */
+   private static int maxCreditAllocation = 100;
+
+   // Used by the broker to decide when to refresh clients credit.  This is not used when client requests credit.
+   private static int minCreditRefresh = 30;
 
    public ProtonServerReceiverContext(AMQPSessionCallback sessionSPI,
                                       AbstractConnectionContext connection,
@@ -50,6 +56,7 @@ public class ProtonServerReceiverContext extends AbstractProtonReceiverContext {
 
    @Override
    public void onFlow(int credits, boolean drain) {
+      flow(Math.min(credits, maxCreditAllocation), maxCreditAllocation);
    }
 
    @Override
@@ -86,10 +93,10 @@ public class ProtonServerReceiverContext extends AbstractProtonReceiverContext {
             catch (Exception e) {
                throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.errorFindingTemporaryQueue(e.getMessage());
             }
+
          }
       }
-
-      flow(numberOfCredits);
+      flow(maxCreditAllocation, minCreditRefresh);
    }
 
    /*
@@ -117,12 +124,8 @@ public class ProtonServerReceiverContext extends AbstractProtonReceiverContext {
                receiver.advance();
 
                sessionSPI.serverSend(receiver, delivery, address, delivery.getMessageFormat(), buffer);
-               delivery.disposition(Accepted.getInstance());
-               delivery.settle();
 
-               if (receiver.getRemoteCredit() < numberOfCredits / 2) {
-                  flow(numberOfCredits);
-               }
+               flow(maxCreditAllocation, minCreditRefresh);
             }
          }
          finally {

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerSenderContext.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerSenderContext.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerSenderContext.java
index 0804084..5fd24d9 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerSenderContext.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerSenderContext.java
@@ -26,6 +26,7 @@ import org.apache.qpid.proton.amqp.messaging.Accepted;
 import org.apache.qpid.proton.amqp.messaging.Modified;
 import org.apache.qpid.proton.amqp.messaging.Rejected;
 import org.apache.qpid.proton.amqp.messaging.Released;
+import org.apache.qpid.proton.amqp.messaging.Source;
 import org.apache.qpid.proton.amqp.messaging.TerminusDurability;
 import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy;
 import org.apache.qpid.proton.amqp.transport.AmqpError;
@@ -40,11 +41,10 @@ import org.proton.plug.AMQPSessionCallback;
 import org.proton.plug.context.AbstractConnectionContext;
 import org.proton.plug.context.AbstractProtonContextSender;
 import org.proton.plug.context.AbstractProtonSessionContext;
+import org.proton.plug.context.ProtonPlugSender;
 import org.proton.plug.exceptions.ActiveMQAMQPException;
 import org.proton.plug.exceptions.ActiveMQAMQPInternalErrorException;
 import org.proton.plug.logger.ActiveMQAMQPProtocolMessageBundle;
-import org.proton.plug.context.ProtonPlugSender;
-import org.apache.qpid.proton.amqp.messaging.Source;
 
 import static org.proton.plug.AmqpSupport.JMS_SELECTOR_FILTER_IDS;
 import static org.proton.plug.AmqpSupport.findFilter;

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/artemis-protocols/artemis-proton-plug/src/test/java/org/proton/plug/test/minimalserver/MinimalSessionSPI.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/test/java/org/proton/plug/test/minimalserver/MinimalSessionSPI.java b/artemis-protocols/artemis-proton-plug/src/test/java/org/proton/plug/test/minimalserver/MinimalSessionSPI.java
index ebc85f1..b917aa6 100644
--- a/artemis-protocols/artemis-proton-plug/src/test/java/org/proton/plug/test/minimalserver/MinimalSessionSPI.java
+++ b/artemis-protocols/artemis-proton-plug/src/test/java/org/proton/plug/test/minimalserver/MinimalSessionSPI.java
@@ -27,9 +27,9 @@ import org.apache.qpid.proton.engine.Receiver;
 import org.apache.qpid.proton.message.ProtonJMessage;
 import org.proton.plug.AMQPSessionCallback;
 import org.proton.plug.AMQPSessionContext;
+import org.proton.plug.SASLResult;
 import org.proton.plug.context.ProtonPlugSender;
 import org.proton.plug.context.server.ProtonServerSessionContext;
-import org.proton.plug.SASLResult;
 import org.proton.plug.util.ProtonServerMessage;
 
 public class MinimalSessionSPI implements AMQPSessionCallback {
@@ -76,6 +76,11 @@ public class MinimalSessionSPI implements AMQPSessionCallback {
    }
 
    @Override
+   public void offerProducerCredit(String address, int credits, int threshold, Receiver receiver) {
+
+   }
+
+   @Override
    public void createTemporaryQueue(String address, String queueName) throws Exception {
 
    }

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/docs/user-manual/en/flow-control.md
----------------------------------------------------------------------
diff --git a/docs/user-manual/en/flow-control.md b/docs/user-manual/en/flow-control.md
index 054bcce..c1b4035 100644
--- a/docs/user-manual/en/flow-control.md
+++ b/docs/user-manual/en/flow-control.md
@@ -273,6 +273,28 @@ control.
 > a misbehaving client to ignore the flow control credits issued by the broker
 > and continue sending with out sufficient credit.
 
+#### Blocking producer window based flow control using AMQP
+
+Apache ActiveMQ Artemis ships with out of the box with 2 protocols that support
+flow control.  Artemis CORE protocol and AMQP.  Both protocols implement flow
+control slightly differently and therefore address full BLOCK policy behaves
+slightly different for clients uses each protocol respectively.
+
+As explained earlier in this chapter the CORE protocol uses a producer window size
+flow control system.  Where credits (representing bytes) are allocated to producers,
+if a producer wants to send a message it should wait until it has enough bytes available
+to send it.  AMQP flow control credits are not representative of bytes but instead represent
+the number of messages a producer is permitted to send (regardless of size).
+
+BLOCK for AMQP works mostly in the same way as the producer window size mechanism above.  Artemis
+will issue 100 credits to a client at a time and refresh them when the clients credits reaches 30.
+The broker will stop issuing credits once an address is full.  However, since AMQP credits represent
+whole messages and not bytes, it would be possible for an AMQP client to significantly exceed an
+address upper bound should the broker continue accepting messages until the clients credits are exhausted.
+For this reason once an address has reached it's upper bound and is blocked (when using AMQP) Artemis
+will start rejecting messages until the address becomes unblocked.  This should be taken into consideration when writing
+application code.
+
 ### Rate limited flow control
 
 Apache ActiveMQ Artemis also allows the rate a producer can emit message to be limited,

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/4d60ced5/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
----------------------------------------------------------------------
diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
index 4d41ff5..8874271 100644
--- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
+++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
@@ -29,11 +29,15 @@ import javax.jms.MessageConsumer;
 import javax.jms.MessageProducer;
 import javax.jms.ObjectMessage;
 import javax.jms.QueueBrowser;
+import javax.jms.ResourceAllocationException;
 import javax.jms.Session;
 import javax.jms.StreamMessage;
 import javax.jms.TemporaryQueue;
 import javax.jms.TextMessage;
+import java.io.IOException;
 import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -48,9 +52,17 @@ import org.apache.activemq.artemis.api.core.TransportConfiguration;
 import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
 import org.apache.activemq.artemis.core.server.ActiveMQServer;
 import org.apache.activemq.artemis.core.server.Queue;
+import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy;
+import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
 import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
 import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
 import org.apache.activemq.artemis.utils.ByteUtil;
+import org.apache.activemq.transport.amqp.client.AmqpClient;
+import org.apache.activemq.transport.amqp.client.AmqpConnection;
+import org.apache.activemq.transport.amqp.client.AmqpMessage;
+import org.apache.activemq.transport.amqp.client.AmqpReceiver;
+import org.apache.activemq.transport.amqp.client.AmqpSender;
+import org.apache.activemq.transport.amqp.client.AmqpSession;
 import org.apache.qpid.jms.JmsConnectionFactory;
 import org.apache.qpid.proton.amqp.messaging.AmqpValue;
 import org.apache.qpid.proton.amqp.messaging.Properties;
@@ -66,12 +78,21 @@ import org.proton.plug.AMQPClientConnectionContext;
 import org.proton.plug.AMQPClientReceiverContext;
 import org.proton.plug.AMQPClientSenderContext;
 import org.proton.plug.AMQPClientSessionContext;
+import org.proton.plug.context.server.ProtonServerReceiverContext;
 import org.proton.plug.test.Constants;
 import org.proton.plug.test.minimalclient.SimpleAMQPConnector;
 
 @RunWith(Parameterized.class)
 public class ProtonTest extends ActiveMQTestBase {
 
+   private static final String amqpConnectionUri = "amqp://localhost:5672";
+
+   private static final String tcpAmqpConnectionUri = "tcp://localhost:5672";
+
+   private static final String userName = "guest";
+
+   private static final String password = "guest";
+
    // this will ensure that all tests in this class are run twice,
    // once with "true" passed to the class' constructor and once with "false"
    @Parameterized.Parameters(name = "{0}")
@@ -106,6 +127,7 @@ public class ProtonTest extends ActiveMQTestBase {
    public void setUp() throws Exception {
       super.setUp();
       disableCheckThread();
+
       server = this.createServer(true, true);
       HashMap<String, Object> params = new HashMap<>();
       params.put(TransportConstants.PORT_PROP_NAME, "5672");
@@ -113,6 +135,12 @@ public class ProtonTest extends ActiveMQTestBase {
       TransportConfiguration transportConfiguration = new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params);
 
       server.getConfiguration().getAcceptorConfigurations().add(transportConfiguration);
+
+      AddressSettings addressSettings = new AddressSettings();
+      addressSettings.setAddressFullMessagePolicy(AddressFullMessagePolicy.BLOCK);
+      addressSettings.setMaxSizeBytes(1 * 1024 * 1024);
+      server.getConfiguration().getAddressesSettings().put("#", addressSettings);
+
       server.start();
       server.createQueue(new SimpleString(coreAddress), new SimpleString(coreAddress), null, true, false);
       server.createQueue(new SimpleString(coreAddress + "1"), new SimpleString(coreAddress + "1"), null, true, false);
@@ -167,7 +195,7 @@ public class ProtonTest extends ActiveMQTestBase {
       maxCreditAllocation.setInt(null, 1);
 
       String destinationAddress = address + 1;
-      AmqpClient client = new AmqpClient(new URI("tcp://localhost:5672"), userName, password);
+      AmqpClient client = new AmqpClient(new URI(tcpAmqpConnectionUri), userName, password);
       AmqpConnection amqpConnection = client.connect();
       try {
          AmqpSession session = amqpConnection.createSession();
@@ -197,9 +225,158 @@ public class ProtonTest extends ActiveMQTestBase {
 
       message = (TextMessage) cons.receive(5000);
       Assert.assertNotNull(message);
+   }
+
+   @Test
+   public void testResourceLimitExceptionOnAddressFull() throws Exception {
+      if (protocol != 0 && protocol != 3) return; // Only run this test for AMQP protocol
+      fillAddress(address + 1);
+   }
+
+   @Test
+   public void testAddressIsBlockedForOtherProdudersWhenFull() throws Exception {
+      if (protocol != 0 && protocol != 3) return; // Only run this test for AMQP protocol
+      String destinationAddress = address + 1;
+      fillAddress(destinationAddress);
+
+      Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+      Exception e = null;
+      try {
+         Destination d = session.createQueue(destinationAddress);
+         MessageProducer p = session.createProducer(d);
+         p.send(session.createBytesMessage());
+      }
+      catch (ResourceAllocationException rae) {
+         e = rae;
+      }
+      assertTrue(e instanceof ResourceAllocationException);
+      assertTrue(e.getMessage().contains("resource-limit-exceeded"));
+   }
+
+   @Test
+   public void testCreditsAreNotAllocatedWhenAddressIsFull() throws Exception {
+      if (protocol != 0 && protocol != 3) return; // Only run this test for AMQP protocol
+
+      // Only allow 1 credit to be submitted at a time.
+      Field maxCreditAllocation = ProtonServerReceiverContext.class.getDeclaredField("maxCreditAllocation");
+      maxCreditAllocation.setAccessible(true);
+      int originalMaxCreditAllocation = maxCreditAllocation.getInt(null);
+      maxCreditAllocation.setInt(null, 1);
+
+      String destinationAddress = address + 1;
+      AmqpClient client = new AmqpClient(new URI(tcpAmqpConnectionUri), userName, password);
+      AmqpConnection amqpConnection = client.connect();
+      try {
+         AmqpSession session = amqpConnection.createSession();
+         AmqpSender sender = session.createSender(destinationAddress);
+         sender.setSendTimeout(1000);
+         sendUntilFull(sender);
+         assertTrue(sender.getSender().getCredit() <= 0);
+      }
+      finally {
+         amqpConnection.close();
+         maxCreditAllocation.setInt(null, originalMaxCreditAllocation);
+      }
+   }
+
+   @Test
+   public void testCreditsAreRefreshedWhenAddressIsUnblocked() throws Exception {
+      if (protocol != 0 && protocol != 3) return; // Only run this test for AMQP protocol
 
+      String destinationAddress = address + 1;
+      int messagesSent = fillAddress(destinationAddress);
+
+      AmqpConnection amqpConnection = null;
+      try {
+         amqpConnection = AmqpClient.connect(new URI(tcpAmqpConnectionUri));
+         AmqpSession session = amqpConnection.createSession();
+         AmqpSender sender = session.createSender(destinationAddress);
+
+         // Wait for a potential flow frame.
+         Thread.sleep(500);
+         assertEquals(0, sender.getSender().getCredit());
+
+         // Empty Address except for 1 message used later.
+         AmqpReceiver receiver = session.createReceiver(destinationAddress);
+         receiver.flow(100);
+
+         AmqpMessage m;
+         for (int i = 0; i < messagesSent - 1; i++) {
+            m = receiver.receive();
+            m.accept();
+         }
+
+         // Wait for address to unblock and flow frame to arrive
+         Thread.sleep(500);
+         assertTrue(sender.getSender().getCredit() > 0);
+         assertNotNull(receiver.receive());
+      }
+      finally {
+         amqpConnection.close();
+      }
+   }
+
+   @Test
+   public void testNewLinkAttachAreNotAllocatedCreditsWhenAddressIsBlocked() throws Exception {
+      if (protocol != 0 && protocol != 3) return; // Only run this test for AMQP protocol
+
+      fillAddress(address + 1);
+      AmqpConnection amqpConnection = null;
+      try {
+         amqpConnection = AmqpClient.connect(new URI(tcpAmqpConnectionUri));
+         AmqpSession session = amqpConnection.createSession();
+         AmqpSender sender = session.createSender(address + 1);
+         // Wait for a potential flow frame.
+         Thread.sleep(1000);
+         assertEquals(0, sender.getSender().getCredit());
+      }
+      finally {
+         amqpConnection.close();
+      }
    }
 
+   /**
+    * Fills an address.  Careful when using this method.  Only use when rejected messages are switched on.
+    * @param address
+    * @return
+    * @throws Exception
+    */
+   private int fillAddress(String address) throws Exception {
+      AmqpClient client = new AmqpClient(new URI(tcpAmqpConnectionUri), userName, password);
+      AmqpConnection amqpConnection = client.connect();
+      try {
+         AmqpSession session = amqpConnection.createSession();
+         AmqpSender sender = session.createSender(address);
+         return sendUntilFull(sender);
+      }
+      finally {
+         amqpConnection.close();
+      }
+   }
+
+   private int sendUntilFull(AmqpSender sender) throws IOException {
+      AmqpMessage message = new AmqpMessage();
+      byte[] payload = new byte[50 * 1024];
+
+      int sentMessages = 0;
+      int maxMessages = 50;
+
+      Exception e = null;
+      try {
+         for (int i = 0; i < maxMessages; i++) {
+            message.setBytes(payload);
+            sender.send(message);
+            sentMessages++;
+         }
+      }
+      catch (IOException ioe) {
+         e = ioe;
+      }
+
+      assertNotNull(e);
+      assertTrue(e.getMessage().contains("amqp:resource-limit-exceeded"));
+      return sentMessages;
+   }
 
    @Test
    public void testReplyTo() throws Throwable {
@@ -918,7 +1095,7 @@ public class ProtonTest extends ActiveMQTestBase {
    private javax.jms.Connection createConnection() throws JMSException {
       Connection connection;
       if (protocol == 3) {
-         factory = new JmsConnectionFactory("amqp://localhost:5672");
+         factory = new JmsConnectionFactory(amqpConnectionUri);
          connection = factory.createConnection();
          connection.setExceptionListener(new ExceptionListener() {
             @Override
@@ -929,7 +1106,7 @@ public class ProtonTest extends ActiveMQTestBase {
          connection.start();
       }
       else if (protocol == 0) {
-         factory = new JmsConnectionFactory("guest", "guest", "amqp://localhost:5672");
+         factory = new JmsConnectionFactory(userName, password, amqpConnectionUri);
          connection = factory.createConnection();
          connection.setExceptionListener(new ExceptionListener() {
             @Override
@@ -950,7 +1127,7 @@ public class ProtonTest extends ActiveMQTestBase {
             factory = new ActiveMQConnectionFactory();
          }
 
-         connection = factory.createConnection("guest", "guest");
+         connection = factory.createConnection(userName, password);
          connection.setExceptionListener(new ExceptionListener() {
             @Override
             public void onException(JMSException exception) {


[2/9] activemq-artemis git commit: ARTEMIS-638 Only allocate credits once Link Attach

Posted by an...@apache.org.
ARTEMIS-638 Only allocate credits once Link Attach


Project: http://git-wip-us.apache.org/repos/asf/activemq-artemis/repo
Commit: http://git-wip-us.apache.org/repos/asf/activemq-artemis/commit/5dfa1c59
Tree: http://git-wip-us.apache.org/repos/asf/activemq-artemis/tree/5dfa1c59
Diff: http://git-wip-us.apache.org/repos/asf/activemq-artemis/diff/5dfa1c59

Branch: refs/heads/master
Commit: 5dfa1c59fb2d718b26d77931b4d562c40fd74256
Parents: df41a60
Author: Martyn Taylor <mt...@redhat.com>
Authored: Mon Jul 18 13:54:41 2016 +0100
Committer: Andy Taylor <an...@gmail.com>
Committed: Wed Jul 20 10:33:44 2016 +0100

----------------------------------------------------------------------
 .../server/ProtonServerConnectionContext.java   |  1 -
 .../tests/integration/proton/ProtonTest.java    | 24 ++++++++++++++++++++
 2 files changed, 24 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/5dfa1c59/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerConnectionContext.java
----------------------------------------------------------------------
diff --git a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerConnectionContext.java b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerConnectionContext.java
index db04a8a..b7d2a98 100644
--- a/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerConnectionContext.java
+++ b/artemis-protocols/artemis-proton-plug/src/main/java/org/proton/plug/context/server/ProtonServerConnectionContext.java
@@ -69,7 +69,6 @@ public class ProtonServerConnectionContext extends AbstractConnectionContext imp
          }
          else {
             protonSession.addReceiver(receiver);
-            receiver.flow(100);
          }
       }
       else {

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/5dfa1c59/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
----------------------------------------------------------------------
diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
index d803e9e..4d41ff5 100644
--- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
+++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/proton/ProtonTest.java
@@ -157,6 +157,30 @@ public class ProtonTest extends ActiveMQTestBase {
    }
 
    @Test
+   public void testCreditsAreAllocatedOnlyOnceOnLinkCreate() throws Exception {
+      if (protocol != 0 && protocol != 3) return; // Only run this test for AMQP protocol
+
+      // Only allow 1 credit to be submitted at a time.
+      Field maxCreditAllocation = ProtonServerReceiverContext.class.getDeclaredField("maxCreditAllocation");
+      maxCreditAllocation.setAccessible(true);
+      int originalMaxCreditAllocation = maxCreditAllocation.getInt(null);
+      maxCreditAllocation.setInt(null, 1);
+
+      String destinationAddress = address + 1;
+      AmqpClient client = new AmqpClient(new URI("tcp://localhost:5672"), userName, password);
+      AmqpConnection amqpConnection = client.connect();
+      try {
+         AmqpSession session = amqpConnection.createSession();
+         AmqpSender sender = session.createSender(destinationAddress);
+         assertTrue(sender.getSender().getCredit() == 1);
+      }
+      finally {
+         amqpConnection.close();
+         maxCreditAllocation.setInt(null, originalMaxCreditAllocation);
+      }
+   }
+
+   @Test
    public void testTemporaryQueue() throws Throwable {
 
       Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);


[6/9] activemq-artemis git commit: ARTEMIS-637 Port 5.x AMQP test client

Posted by an...@apache.org.
http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTcpTransport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTcpTransport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTcpTransport.java
new file mode 100644
index 0000000..f790433
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTcpTransport.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.activemq.transport.amqp.client.transport;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.FixedRecvByteBufAllocator;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import org.apache.activemq.transport.amqp.client.util.IOExceptionSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TCP based transport that uses Netty as the underlying IO layer.
+ */
+public class NettyTcpTransport implements NettyTransport {
+
+   private static final Logger LOG = LoggerFactory.getLogger(NettyTcpTransport.class);
+
+   private static final int QUIET_PERIOD = 20;
+   private static final int SHUTDOWN_TIMEOUT = 100;
+
+   protected Bootstrap bootstrap;
+   protected EventLoopGroup group;
+   protected Channel channel;
+   protected NettyTransportListener listener;
+   protected NettyTransportOptions options;
+   protected final URI remote;
+   protected boolean secure;
+
+   private final AtomicBoolean connected = new AtomicBoolean();
+   private final AtomicBoolean closed = new AtomicBoolean();
+   private final CountDownLatch connectLatch = new CountDownLatch(1);
+   private IOException failureCause;
+   private Throwable pendingFailure;
+
+   /**
+    * Create a new transport instance
+    *
+    * @param remoteLocation the URI that defines the remote resource to connect to.
+    * @param options        the transport options used to configure the socket connection.
+    */
+   public NettyTcpTransport(URI remoteLocation, NettyTransportOptions options) {
+      this(null, remoteLocation, options);
+   }
+
+   /**
+    * Create a new transport instance
+    *
+    * @param listener       the TransportListener that will receive events from this Transport.
+    * @param remoteLocation the URI that defines the remote resource to connect to.
+    * @param options        the transport options used to configure the socket connection.
+    */
+   public NettyTcpTransport(NettyTransportListener listener, URI remoteLocation, NettyTransportOptions options) {
+      this.options = options;
+      this.listener = listener;
+      this.remote = remoteLocation;
+      this.secure = remoteLocation.getScheme().equalsIgnoreCase("ssl");
+   }
+
+   @Override
+   public void connect() throws IOException {
+
+      if (listener == null) {
+         throw new IllegalStateException("A transport listener must be set before connection attempts.");
+      }
+
+      group = new NioEventLoopGroup(1);
+
+      bootstrap = new Bootstrap();
+      bootstrap.group(group);
+      bootstrap.channel(NioSocketChannel.class);
+      bootstrap.handler(new ChannelInitializer<Channel>() {
+
+         @Override
+         public void initChannel(Channel connectedChannel) throws Exception {
+            configureChannel(connectedChannel);
+         }
+      });
+
+      configureNetty(bootstrap, getTransportOptions());
+
+      ChannelFuture future = bootstrap.connect(getRemoteHost(), getRemotePort());
+      future.addListener(new ChannelFutureListener() {
+
+         @Override
+         public void operationComplete(ChannelFuture future) throws Exception {
+            if (future.isSuccess()) {
+               handleConnected(future.channel());
+            }
+            else if (future.isCancelled()) {
+               connectionFailed(future.channel(), new IOException("Connection attempt was cancelled"));
+            }
+            else {
+               connectionFailed(future.channel(), IOExceptionSupport.create(future.cause()));
+            }
+         }
+      });
+
+      try {
+         connectLatch.await();
+      }
+      catch (InterruptedException ex) {
+         LOG.debug("Transport connection was interrupted.");
+         Thread.interrupted();
+         failureCause = IOExceptionSupport.create(ex);
+      }
+
+      if (failureCause != null) {
+         // Close out any Netty resources now as they are no longer needed.
+         if (channel != null) {
+            channel.close().syncUninterruptibly();
+            channel = null;
+         }
+         if (group != null) {
+            group.shutdownGracefully(QUIET_PERIOD, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
+            group = null;
+         }
+
+         throw failureCause;
+      }
+      else {
+         // Connected, allow any held async error to fire now and close the transport.
+         channel.eventLoop().execute(new Runnable() {
+
+            @Override
+            public void run() {
+               if (pendingFailure != null) {
+                  channel.pipeline().fireExceptionCaught(pendingFailure);
+               }
+            }
+         });
+      }
+   }
+
+   @Override
+   public boolean isConnected() {
+      return connected.get();
+   }
+
+   @Override
+   public boolean isSSL() {
+      return secure;
+   }
+
+   @Override
+   public void close() throws IOException {
+      if (closed.compareAndSet(false, true)) {
+         connected.set(false);
+         if (channel != null) {
+            channel.close().syncUninterruptibly();
+         }
+         if (group != null) {
+            group.shutdownGracefully(QUIET_PERIOD, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
+         }
+      }
+   }
+
+   @Override
+   public ByteBuf allocateSendBuffer(int size) throws IOException {
+      checkConnected();
+      return channel.alloc().ioBuffer(size, size);
+   }
+
+   @Override
+   public void send(ByteBuf output) throws IOException {
+      checkConnected();
+      int length = output.readableBytes();
+      if (length == 0) {
+         return;
+      }
+
+      LOG.trace("Attempted write of: {} bytes", length);
+
+      channel.writeAndFlush(output);
+   }
+
+   @Override
+   public NettyTransportListener getTransportListener() {
+      return listener;
+   }
+
+   @Override
+   public void setTransportListener(NettyTransportListener listener) {
+      this.listener = listener;
+   }
+
+   @Override
+   public NettyTransportOptions getTransportOptions() {
+      if (options == null) {
+         if (isSSL()) {
+            options = NettyTransportSslOptions.INSTANCE;
+         }
+         else {
+            options = NettyTransportOptions.INSTANCE;
+         }
+      }
+
+      return options;
+   }
+
+   @Override
+   public URI getRemoteLocation() {
+      return remote;
+   }
+
+   @Override
+   public Principal getLocalPrincipal() {
+      if (!isSSL()) {
+         throw new UnsupportedOperationException("Not connected to a secure channel");
+      }
+
+      SslHandler sslHandler = channel.pipeline().get(SslHandler.class);
+
+      return sslHandler.engine().getSession().getLocalPrincipal();
+   }
+
+   //----- Internal implementation details, can be overridden as needed --//
+
+   protected String getRemoteHost() {
+      return remote.getHost();
+   }
+
+   protected int getRemotePort() {
+      int port = remote.getPort();
+
+      if (port <= 0) {
+         if (isSSL()) {
+            port = getSslOptions().getDefaultSslPort();
+         }
+         else {
+            port = getTransportOptions().getDefaultTcpPort();
+         }
+      }
+
+      return port;
+   }
+
+   protected void configureNetty(Bootstrap bootstrap, NettyTransportOptions options) {
+      bootstrap.option(ChannelOption.TCP_NODELAY, options.isTcpNoDelay());
+      bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, options.getConnectTimeout());
+      bootstrap.option(ChannelOption.SO_KEEPALIVE, options.isTcpKeepAlive());
+      bootstrap.option(ChannelOption.SO_LINGER, options.getSoLinger());
+      bootstrap.option(ChannelOption.ALLOCATOR, PartialPooledByteBufAllocator.INSTANCE);
+
+      if (options.getSendBufferSize() != -1) {
+         bootstrap.option(ChannelOption.SO_SNDBUF, options.getSendBufferSize());
+      }
+
+      if (options.getReceiveBufferSize() != -1) {
+         bootstrap.option(ChannelOption.SO_RCVBUF, options.getReceiveBufferSize());
+         bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(options.getReceiveBufferSize()));
+      }
+
+      if (options.getTrafficClass() != -1) {
+         bootstrap.option(ChannelOption.IP_TOS, options.getTrafficClass());
+      }
+   }
+
+   protected void configureChannel(final Channel channel) throws Exception {
+      if (isSSL()) {
+         SslHandler sslHandler = NettyTransportSupport.createSslHandler(getRemoteLocation(), getSslOptions());
+         sslHandler.handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {
+            @Override
+            public void operationComplete(Future<Channel> future) throws Exception {
+               if (future.isSuccess()) {
+                  LOG.trace("SSL Handshake has completed: {}", channel);
+                  connectionEstablished(channel);
+               }
+               else {
+                  LOG.trace("SSL Handshake has failed: {}", channel);
+                  connectionFailed(channel, IOExceptionSupport.create(future.cause()));
+               }
+            }
+         });
+
+         channel.pipeline().addLast(sslHandler);
+      }
+
+      channel.pipeline().addLast(new NettyTcpTransportHandler());
+   }
+
+   protected void handleConnected(final Channel channel) throws Exception {
+      if (!isSSL()) {
+         connectionEstablished(channel);
+      }
+   }
+
+   //----- State change handlers and checks ---------------------------------//
+
+   /**
+    * Called when the transport has successfully connected and is ready for use.
+    */
+   protected void connectionEstablished(Channel connectedChannel) {
+      channel = connectedChannel;
+      connected.set(true);
+      connectLatch.countDown();
+   }
+
+   /**
+    * Called when the transport connection failed and an error should be returned.
+    *
+    * @param failedChannel The Channel instance that failed.
+    * @param cause         An IOException that describes the cause of the failed connection.
+    */
+   protected void connectionFailed(Channel failedChannel, IOException cause) {
+      failureCause = IOExceptionSupport.create(cause);
+      channel = failedChannel;
+      connected.set(false);
+      connectLatch.countDown();
+   }
+
+   private NettyTransportSslOptions getSslOptions() {
+      return (NettyTransportSslOptions) getTransportOptions();
+   }
+
+   private void checkConnected() throws IOException {
+      if (!connected.get()) {
+         throw new IOException("Cannot send to a non-connected transport.");
+      }
+   }
+
+   //----- Handle connection events -----------------------------------------//
+
+   private class NettyTcpTransportHandler extends SimpleChannelInboundHandler<ByteBuf> {
+
+      @Override
+      public void channelActive(ChannelHandlerContext context) throws Exception {
+         LOG.trace("Channel has become active! Channel is {}", context.channel());
+      }
+
+      @Override
+      public void channelInactive(ChannelHandlerContext context) throws Exception {
+         LOG.trace("Channel has gone inactive! Channel is {}", context.channel());
+         if (connected.compareAndSet(true, false) && !closed.get()) {
+            LOG.trace("Firing onTransportClosed listener");
+            listener.onTransportClosed();
+         }
+      }
+
+      @Override
+      public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
+         LOG.trace("Exception on channel! Channel is {}", context.channel());
+         if (connected.compareAndSet(true, false) && !closed.get()) {
+            LOG.trace("Firing onTransportError listener");
+            if (pendingFailure != null) {
+               listener.onTransportError(pendingFailure);
+            }
+            else {
+               listener.onTransportError(cause);
+            }
+         }
+         else {
+            // Hold the first failure for later dispatch if connect succeeds.
+            // This will then trigger disconnect using the first error reported.
+            if (pendingFailure != null) {
+               LOG.trace("Holding error until connect succeeds: {}", cause.getMessage());
+               pendingFailure = cause;
+            }
+         }
+      }
+
+      @Override
+      protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
+         LOG.trace("New data read: {} bytes incoming: {}", buffer.readableBytes(), buffer);
+         listener.onData(buffer);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransport.java
new file mode 100644
index 0000000..a2bacdc
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransport.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.activemq.transport.amqp.client.transport;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ *
+ */
+public interface NettyTransport {
+
+   void connect() throws IOException;
+
+   boolean isConnected();
+
+   boolean isSSL();
+
+   void close() throws IOException;
+
+   ByteBuf allocateSendBuffer(int size) throws IOException;
+
+   void send(ByteBuf output) throws IOException;
+
+   NettyTransportListener getTransportListener();
+
+   void setTransportListener(NettyTransportListener listener);
+
+   NettyTransportOptions getTransportOptions();
+
+   URI getRemoteLocation();
+
+   Principal getLocalPrincipal();
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportFactory.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportFactory.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportFactory.java
new file mode 100644
index 0000000..5663713
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportFactory.java
@@ -0,0 +1,80 @@
+/**
+ * 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.activemq.transport.amqp.client.transport;
+
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.activemq.transport.amqp.client.util.PropertyUtil;
+
+/**
+ * Factory for creating the Netty based TCP Transport.
+ */
+public final class NettyTransportFactory {
+
+   private NettyTransportFactory() {
+   }
+
+   /**
+    * Creates an instance of the given Transport and configures it using the
+    * properties set on the given remote broker URI.
+    *
+    * @param remoteURI The URI used to connect to a remote Peer.
+    * @return a new Transport instance.
+    * @throws Exception if an error occurs while creating the Transport instance.
+    */
+   public static NettyTransport createTransport(URI remoteURI) throws Exception {
+      Map<String, String> map = PropertyUtil.parseQuery(remoteURI.getQuery());
+      Map<String, String> transportURIOptions = PropertyUtil.filterProperties(map, "transport.");
+      NettyTransportOptions transportOptions = null;
+
+      remoteURI = PropertyUtil.replaceQuery(remoteURI, map);
+
+      if (!remoteURI.getScheme().equalsIgnoreCase("ssl") && !remoteURI.getScheme().equalsIgnoreCase("wss")) {
+         transportOptions = NettyTransportOptions.INSTANCE.clone();
+      }
+      else {
+         transportOptions = NettyTransportSslOptions.INSTANCE.clone();
+      }
+
+      Map<String, String> unused = PropertyUtil.setProperties(transportOptions, transportURIOptions);
+      if (!unused.isEmpty()) {
+         String msg = " Not all transport options could be set on the TCP based" +
+            " Transport. Check the options are spelled correctly." +
+            " Unused parameters=[" + unused + "]." +
+            " This provider instance cannot be started.";
+         throw new IllegalArgumentException(msg);
+      }
+
+      NettyTransport result = null;
+
+      switch (remoteURI.getScheme().toLowerCase()) {
+         case "tcp":
+         case "ssl":
+            result = new NettyTcpTransport(remoteURI, transportOptions);
+            break;
+         case "ws":
+         case "wss":
+            result = new NettyWSTransport(remoteURI, transportOptions);
+            break;
+         default:
+            throw new IllegalArgumentException("Invalid URI Scheme: " + remoteURI.getScheme());
+      }
+
+      return result;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportListener.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportListener.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportListener.java
new file mode 100644
index 0000000..c23ca8c
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportListener.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.activemq.transport.amqp.client.transport;
+
+import io.netty.buffer.ByteBuf;
+
+/**
+ * Listener interface that should be implemented by users of the various
+ * QpidJMS Transport classes.
+ */
+public interface NettyTransportListener {
+
+   /**
+    * Called when new incoming data has become available.
+    *
+    * @param incoming the next incoming packet of data.
+    */
+   void onData(ByteBuf incoming);
+
+   /**
+    * Called if the connection state becomes closed.
+    */
+   void onTransportClosed();
+
+   /**
+    * Called when an error occurs during normal Transport operations.
+    *
+    * @param cause the error that triggered this event.
+    */
+   void onTransportError(Throwable cause);
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportOptions.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportOptions.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportOptions.java
new file mode 100644
index 0000000..3ffb8c8
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportOptions.java
@@ -0,0 +1,177 @@
+/**
+ * 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.activemq.transport.amqp.client.transport;
+
+/**
+ * Encapsulates all the TCP Transport options in one configuration object.
+ */
+public class NettyTransportOptions implements Cloneable {
+
+   public static final int DEFAULT_SEND_BUFFER_SIZE = 64 * 1024;
+   public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_SEND_BUFFER_SIZE;
+   public static final int DEFAULT_TRAFFIC_CLASS = 0;
+   public static final boolean DEFAULT_TCP_NO_DELAY = true;
+   public static final boolean DEFAULT_TCP_KEEP_ALIVE = false;
+   public static final int DEFAULT_SO_LINGER = Integer.MIN_VALUE;
+   public static final int DEFAULT_SO_TIMEOUT = -1;
+   public static final int DEFAULT_CONNECT_TIMEOUT = 60000;
+   public static final int DEFAULT_TCP_PORT = 5672;
+
+   public static final NettyTransportOptions INSTANCE = new NettyTransportOptions();
+
+   private int sendBufferSize = DEFAULT_SEND_BUFFER_SIZE;
+   private int receiveBufferSize = DEFAULT_RECEIVE_BUFFER_SIZE;
+   private int trafficClass = DEFAULT_TRAFFIC_CLASS;
+   private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;
+   private int soTimeout = DEFAULT_SO_TIMEOUT;
+   private int soLinger = DEFAULT_SO_LINGER;
+   private boolean tcpKeepAlive = DEFAULT_TCP_KEEP_ALIVE;
+   private boolean tcpNoDelay = DEFAULT_TCP_NO_DELAY;
+   private int defaultTcpPort = DEFAULT_TCP_PORT;
+
+   /**
+    * @return the currently set send buffer size in bytes.
+    */
+   public int getSendBufferSize() {
+      return sendBufferSize;
+   }
+
+   /**
+    * Sets the send buffer size in bytes, the value must be greater than zero
+    * or an {@link IllegalArgumentException} will be thrown.
+    *
+    * @param sendBufferSize the new send buffer size for the TCP Transport.
+    * @throws IllegalArgumentException if the value given is not in the valid range.
+    */
+   public void setSendBufferSize(int sendBufferSize) {
+      if (sendBufferSize <= 0) {
+         throw new IllegalArgumentException("The send buffer size must be > 0");
+      }
+
+      this.sendBufferSize = sendBufferSize;
+   }
+
+   /**
+    * @return the currently configured receive buffer size in bytes.
+    */
+   public int getReceiveBufferSize() {
+      return receiveBufferSize;
+   }
+
+   /**
+    * Sets the receive buffer size in bytes, the value must be greater than zero
+    * or an {@link IllegalArgumentException} will be thrown.
+    *
+    * @param receiveBufferSize the new receive buffer size for the TCP Transport.
+    * @throws IllegalArgumentException if the value given is not in the valid range.
+    */
+   public void setReceiveBufferSize(int receiveBufferSize) {
+      if (receiveBufferSize <= 0) {
+         throw new IllegalArgumentException("The send buffer size must be > 0");
+      }
+
+      this.receiveBufferSize = receiveBufferSize;
+   }
+
+   /**
+    * @return the currently configured traffic class value.
+    */
+   public int getTrafficClass() {
+      return trafficClass;
+   }
+
+   /**
+    * Sets the traffic class value used by the TCP connection, valid
+    * range is between 0 and 255.
+    *
+    * @param trafficClass the new traffic class value.
+    * @throws IllegalArgumentException if the value given is not in the valid range.
+    */
+   public void setTrafficClass(int trafficClass) {
+      if (trafficClass < 0 || trafficClass > 255) {
+         throw new IllegalArgumentException("Traffic class must be in the range [0..255]");
+      }
+
+      this.trafficClass = trafficClass;
+   }
+
+   public int getSoTimeout() {
+      return soTimeout;
+   }
+
+   public void setSoTimeout(int soTimeout) {
+      this.soTimeout = soTimeout;
+   }
+
+   public boolean isTcpNoDelay() {
+      return tcpNoDelay;
+   }
+
+   public void setTcpNoDelay(boolean tcpNoDelay) {
+      this.tcpNoDelay = tcpNoDelay;
+   }
+
+   public int getSoLinger() {
+      return soLinger;
+   }
+
+   public void setSoLinger(int soLinger) {
+      this.soLinger = soLinger;
+   }
+
+   public boolean isTcpKeepAlive() {
+      return tcpKeepAlive;
+   }
+
+   public void setTcpKeepAlive(boolean keepAlive) {
+      this.tcpKeepAlive = keepAlive;
+   }
+
+   public int getConnectTimeout() {
+      return connectTimeout;
+   }
+
+   public void setConnectTimeout(int connectTimeout) {
+      this.connectTimeout = connectTimeout;
+   }
+
+   public int getDefaultTcpPort() {
+      return defaultTcpPort;
+   }
+
+   public void setDefaultTcpPort(int defaultTcpPort) {
+      this.defaultTcpPort = defaultTcpPort;
+   }
+
+   @Override
+   public NettyTransportOptions clone() {
+      return copyOptions(new NettyTransportOptions());
+   }
+
+   protected NettyTransportOptions copyOptions(NettyTransportOptions copy) {
+      copy.setConnectTimeout(getConnectTimeout());
+      copy.setReceiveBufferSize(getReceiveBufferSize());
+      copy.setSendBufferSize(getSendBufferSize());
+      copy.setSoLinger(getSoLinger());
+      copy.setSoTimeout(getSoTimeout());
+      copy.setTcpKeepAlive(isTcpKeepAlive());
+      copy.setTcpNoDelay(isTcpNoDelay());
+      copy.setTrafficClass(getTrafficClass());
+
+      return copy;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSslOptions.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSslOptions.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSslOptions.java
new file mode 100644
index 0000000..e256fbb
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSslOptions.java
@@ -0,0 +1,284 @@
+/**
+ * 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.activemq.transport.amqp.client.transport;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Holds the defined SSL options for connections that operate over a secure
+ * transport.  Options are read from the environment and can be overridden by
+ * specifying them on the connection URI.
+ */
+public class NettyTransportSslOptions extends NettyTransportOptions {
+
+   public static final String DEFAULT_STORE_TYPE = "jks";
+   public static final String DEFAULT_CONTEXT_PROTOCOL = "TLS";
+   public static final boolean DEFAULT_TRUST_ALL = false;
+   public static final boolean DEFAULT_VERIFY_HOST = false;
+   public static final List<String> DEFAULT_DISABLED_PROTOCOLS = Collections.unmodifiableList(Arrays.asList(new String[]{"SSLv2Hello", "SSLv3"}));
+   public static final int DEFAULT_SSL_PORT = 5671;
+
+   public static final NettyTransportSslOptions INSTANCE = new NettyTransportSslOptions();
+
+   private String keyStoreLocation;
+   private String keyStorePassword;
+   private String trustStoreLocation;
+   private String trustStorePassword;
+   private String storeType = DEFAULT_STORE_TYPE;
+   private String[] enabledCipherSuites;
+   private String[] disabledCipherSuites;
+   private String[] enabledProtocols;
+   private String[] disabledProtocols = DEFAULT_DISABLED_PROTOCOLS.toArray(new String[0]);
+   private String contextProtocol = DEFAULT_CONTEXT_PROTOCOL;
+
+   private boolean trustAll = DEFAULT_TRUST_ALL;
+   private boolean verifyHost = DEFAULT_VERIFY_HOST;
+   private String keyAlias;
+   private int defaultSslPort = DEFAULT_SSL_PORT;
+
+   static {
+      INSTANCE.setKeyStoreLocation(System.getProperty("javax.net.ssl.keyStore"));
+      INSTANCE.setKeyStorePassword(System.getProperty("javax.net.ssl.keyStorePassword"));
+      INSTANCE.setTrustStoreLocation(System.getProperty("javax.net.ssl.trustStore"));
+      INSTANCE.setTrustStorePassword(System.getProperty("javax.net.ssl.keyStorePassword"));
+   }
+
+   /**
+    * @return the keyStoreLocation currently configured.
+    */
+   public String getKeyStoreLocation() {
+      return keyStoreLocation;
+   }
+
+   /**
+    * Sets the location on disk of the key store to use.
+    *
+    * @param keyStoreLocation the keyStoreLocation to use to create the key manager.
+    */
+   public void setKeyStoreLocation(String keyStoreLocation) {
+      this.keyStoreLocation = keyStoreLocation;
+   }
+
+   /**
+    * @return the keyStorePassword
+    */
+   public String getKeyStorePassword() {
+      return keyStorePassword;
+   }
+
+   /**
+    * @param keyStorePassword the keyStorePassword to set
+    */
+   public void setKeyStorePassword(String keyStorePassword) {
+      this.keyStorePassword = keyStorePassword;
+   }
+
+   /**
+    * @return the trustStoreLocation
+    */
+   public String getTrustStoreLocation() {
+      return trustStoreLocation;
+   }
+
+   /**
+    * @param trustStoreLocation the trustStoreLocation to set
+    */
+   public void setTrustStoreLocation(String trustStoreLocation) {
+      this.trustStoreLocation = trustStoreLocation;
+   }
+
+   /**
+    * @return the trustStorePassword
+    */
+   public String getTrustStorePassword() {
+      return trustStorePassword;
+   }
+
+   /**
+    * @param trustStorePassword the trustStorePassword to set
+    */
+   public void setTrustStorePassword(String trustStorePassword) {
+      this.trustStorePassword = trustStorePassword;
+   }
+
+   /**
+    * @return the storeType
+    */
+   public String getStoreType() {
+      return storeType;
+   }
+
+   /**
+    * @param storeType the format that the store files are encoded in.
+    */
+   public void setStoreType(String storeType) {
+      this.storeType = storeType;
+   }
+
+   /**
+    * @return the enabledCipherSuites
+    */
+   public String[] getEnabledCipherSuites() {
+      return enabledCipherSuites;
+   }
+
+   /**
+    * @param enabledCipherSuites the enabledCipherSuites to set
+    */
+   public void setEnabledCipherSuites(String[] enabledCipherSuites) {
+      this.enabledCipherSuites = enabledCipherSuites;
+   }
+
+   /**
+    * @return the disabledCipherSuites
+    */
+   public String[] getDisabledCipherSuites() {
+      return disabledCipherSuites;
+   }
+
+   /**
+    * @param disabledCipherSuites the disabledCipherSuites to set
+    */
+   public void setDisabledCipherSuites(String[] disabledCipherSuites) {
+      this.disabledCipherSuites = disabledCipherSuites;
+   }
+
+   /**
+    * @return the enabledProtocols or null if the defaults should be used
+    */
+   public String[] getEnabledProtocols() {
+      return enabledProtocols;
+   }
+
+   /**
+    * The protocols to be set as enabled.
+    *
+    * @param enabledProtocols the enabled protocols to set, or null if the defaults should be used.
+    */
+   public void setEnabledProtocols(String[] enabledProtocols) {
+      this.enabledProtocols = enabledProtocols;
+   }
+
+   /**
+    * @return the protocols to disable or null if none should be
+    */
+   public String[] getDisabledProtocols() {
+      return disabledProtocols;
+   }
+
+   /**
+    * The protocols to be disable.
+    *
+    * @param disabledProtocols the protocols to disable, or null if none should be.
+    */
+   public void setDisabledProtocols(String[] disabledProtocols) {
+      this.disabledProtocols = disabledProtocols;
+   }
+
+   /**
+    * @return the context protocol to use
+    */
+   public String getContextProtocol() {
+      return contextProtocol;
+   }
+
+   /**
+    * The protocol value to use when creating an SSLContext via
+    * SSLContext.getInstance(protocol).
+    *
+    * @param contextProtocol the context protocol to use.
+    */
+   public void setContextProtocol(String contextProtocol) {
+      this.contextProtocol = contextProtocol;
+   }
+
+   /**
+    * @return the trustAll
+    */
+   public boolean isTrustAll() {
+      return trustAll;
+   }
+
+   /**
+    * @param trustAll the trustAll to set
+    */
+   public void setTrustAll(boolean trustAll) {
+      this.trustAll = trustAll;
+   }
+
+   /**
+    * @return the verifyHost
+    */
+   public boolean isVerifyHost() {
+      return verifyHost;
+   }
+
+   /**
+    * @param verifyHost the verifyHost to set
+    */
+   public void setVerifyHost(boolean verifyHost) {
+      this.verifyHost = verifyHost;
+   }
+
+   /**
+    * @return the key alias
+    */
+   public String getKeyAlias() {
+      return keyAlias;
+   }
+
+   /**
+    * @param keyAlias the key alias to use
+    */
+   public void setKeyAlias(String keyAlias) {
+      this.keyAlias = keyAlias;
+   }
+
+   public int getDefaultSslPort() {
+      return defaultSslPort;
+   }
+
+   public void setDefaultSslPort(int defaultSslPort) {
+      this.defaultSslPort = defaultSslPort;
+   }
+
+   @Override
+   public NettyTransportSslOptions clone() {
+      return copyOptions(new NettyTransportSslOptions());
+   }
+
+   protected NettyTransportSslOptions copyOptions(NettyTransportSslOptions copy) {
+      super.copyOptions(copy);
+
+      copy.setKeyStoreLocation(getKeyStoreLocation());
+      copy.setKeyStorePassword(getKeyStorePassword());
+      copy.setTrustStoreLocation(getTrustStoreLocation());
+      copy.setTrustStorePassword(getTrustStorePassword());
+      copy.setStoreType(getStoreType());
+      copy.setEnabledCipherSuites(getEnabledCipherSuites());
+      copy.setDisabledCipherSuites(getDisabledCipherSuites());
+      copy.setEnabledProtocols(getEnabledProtocols());
+      copy.setDisabledProtocols(getDisabledProtocols());
+      copy.setTrustAll(isTrustAll());
+      copy.setVerifyHost(isVerifyHost());
+      copy.setKeyAlias(getKeyAlias());
+      copy.setContextProtocol(getContextProtocol());
+      return copy;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSupport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSupport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSupport.java
new file mode 100644
index 0000000..51cedea
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyTransportSupport.java
@@ -0,0 +1,288 @@
+/**
+ * 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.activemq.transport.amqp.client.transport;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import io.netty.handler.ssl.SslHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Static class that provides various utility methods used by Transport implementations.
+ */
+public class NettyTransportSupport {
+
+   private static final Logger LOG = LoggerFactory.getLogger(NettyTransportSupport.class);
+
+   /**
+    * Creates a Netty SslHandler instance for use in Transports that require
+    * an SSL encoder / decoder.
+    *
+    * @param remote  The URI of the remote peer that the SslHandler will be used against.
+    * @param options The SSL options object to build the SslHandler instance from.
+    * @return a new SslHandler that is configured from the given options.
+    * @throws Exception if an error occurs while creating the SslHandler instance.
+    */
+   public static SslHandler createSslHandler(URI remote, NettyTransportSslOptions options) throws Exception {
+      return new SslHandler(createSslEngine(remote, createSslContext(options), options));
+   }
+
+   /**
+    * Create a new SSLContext using the options specific in the given TransportSslOptions
+    * instance.
+    *
+    * @param options the configured options used to create the SSLContext.
+    * @return a new SSLContext instance.
+    * @throws Exception if an error occurs while creating the context.
+    */
+   public static SSLContext createSslContext(NettyTransportSslOptions options) throws Exception {
+      try {
+         String contextProtocol = options.getContextProtocol();
+         LOG.trace("Getting SSLContext instance using protocol: {}", contextProtocol);
+
+         SSLContext context = SSLContext.getInstance(contextProtocol);
+         KeyManager[] keyMgrs = loadKeyManagers(options);
+         TrustManager[] trustManagers = loadTrustManagers(options);
+
+         context.init(keyMgrs, trustManagers, new SecureRandom());
+         return context;
+      }
+      catch (Exception e) {
+         LOG.error("Failed to create SSLContext: {}", e, e);
+         throw e;
+      }
+   }
+
+   /**
+    * Create a new SSLEngine instance in client mode from the given SSLContext and
+    * TransportSslOptions instances.
+    *
+    * @param context the SSLContext to use when creating the engine.
+    * @param options the TransportSslOptions to use to configure the new SSLEngine.
+    * @return a new SSLEngine instance in client mode.
+    * @throws Exception if an error occurs while creating the new SSLEngine.
+    */
+   public static SSLEngine createSslEngine(SSLContext context, NettyTransportSslOptions options) throws Exception {
+      return createSslEngine(null, context, options);
+   }
+
+   /**
+    * Create a new SSLEngine instance in client mode from the given SSLContext and
+    * TransportSslOptions instances.
+    *
+    * @param remote  the URI of the remote peer that will be used to initialize the engine, may be null if none should.
+    * @param context the SSLContext to use when creating the engine.
+    * @param options the TransportSslOptions to use to configure the new SSLEngine.
+    * @return a new SSLEngine instance in client mode.
+    * @throws Exception if an error occurs while creating the new SSLEngine.
+    */
+   public static SSLEngine createSslEngine(URI remote,
+                                           SSLContext context,
+                                           NettyTransportSslOptions options) throws Exception {
+      SSLEngine engine = null;
+      if (remote == null) {
+         engine = context.createSSLEngine();
+      }
+      else {
+         engine = context.createSSLEngine(remote.getHost(), remote.getPort());
+      }
+
+      engine.setEnabledProtocols(buildEnabledProtocols(engine, options));
+      engine.setEnabledCipherSuites(buildEnabledCipherSuites(engine, options));
+      engine.setUseClientMode(true);
+
+      if (options.isVerifyHost()) {
+         SSLParameters sslParameters = engine.getSSLParameters();
+         sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+         engine.setSSLParameters(sslParameters);
+      }
+
+      return engine;
+   }
+
+   private static String[] buildEnabledProtocols(SSLEngine engine, NettyTransportSslOptions options) {
+      List<String> enabledProtocols = new ArrayList<>();
+
+      if (options.getEnabledProtocols() != null) {
+         List<String> configuredProtocols = Arrays.asList(options.getEnabledProtocols());
+         LOG.trace("Configured protocols from transport options: {}", configuredProtocols);
+         enabledProtocols.addAll(configuredProtocols);
+      }
+      else {
+         List<String> engineProtocols = Arrays.asList(engine.getEnabledProtocols());
+         LOG.trace("Default protocols from the SSLEngine: {}", engineProtocols);
+         enabledProtocols.addAll(engineProtocols);
+      }
+
+      String[] disabledProtocols = options.getDisabledProtocols();
+      if (disabledProtocols != null) {
+         List<String> disabled = Arrays.asList(disabledProtocols);
+         LOG.trace("Disabled protocols: {}", disabled);
+         enabledProtocols.removeAll(disabled);
+      }
+
+      LOG.trace("Enabled protocols: {}", enabledProtocols);
+
+      return enabledProtocols.toArray(new String[0]);
+   }
+
+   private static String[] buildEnabledCipherSuites(SSLEngine engine, NettyTransportSslOptions options) {
+      List<String> enabledCipherSuites = new ArrayList<>();
+
+      if (options.getEnabledCipherSuites() != null) {
+         List<String> configuredCipherSuites = Arrays.asList(options.getEnabledCipherSuites());
+         LOG.trace("Configured cipher suites from transport options: {}", configuredCipherSuites);
+         enabledCipherSuites.addAll(configuredCipherSuites);
+      }
+      else {
+         List<String> engineCipherSuites = Arrays.asList(engine.getEnabledCipherSuites());
+         LOG.trace("Default cipher suites from the SSLEngine: {}", engineCipherSuites);
+         enabledCipherSuites.addAll(engineCipherSuites);
+      }
+
+      String[] disabledCipherSuites = options.getDisabledCipherSuites();
+      if (disabledCipherSuites != null) {
+         List<String> disabled = Arrays.asList(disabledCipherSuites);
+         LOG.trace("Disabled cipher suites: {}", disabled);
+         enabledCipherSuites.removeAll(disabled);
+      }
+
+      LOG.trace("Enabled cipher suites: {}", enabledCipherSuites);
+
+      return enabledCipherSuites.toArray(new String[0]);
+   }
+
+   private static TrustManager[] loadTrustManagers(NettyTransportSslOptions options) throws Exception {
+      if (options.isTrustAll()) {
+         return new TrustManager[]{createTrustAllTrustManager()};
+      }
+
+      if (options.getTrustStoreLocation() == null) {
+         return null;
+      }
+
+      TrustManagerFactory fact = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
+      String storeLocation = options.getTrustStoreLocation();
+      String storePassword = options.getTrustStorePassword();
+      String storeType = options.getStoreType();
+
+      LOG.trace("Attempt to load TrustStore from location {} of type {}", storeLocation, storeType);
+
+      KeyStore trustStore = loadStore(storeLocation, storePassword, storeType);
+      fact.init(trustStore);
+
+      return fact.getTrustManagers();
+   }
+
+   private static KeyManager[] loadKeyManagers(NettyTransportSslOptions options) throws Exception {
+      if (options.getKeyStoreLocation() == null) {
+         return null;
+      }
+
+      KeyManagerFactory fact = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+      String storeLocation = options.getKeyStoreLocation();
+      String storePassword = options.getKeyStorePassword();
+      String storeType = options.getStoreType();
+      String alias = options.getKeyAlias();
+
+      LOG.trace("Attempt to load KeyStore from location {} of type {}", storeLocation, storeType);
+
+      KeyStore keyStore = loadStore(storeLocation, storePassword, storeType);
+      fact.init(keyStore, storePassword != null ? storePassword.toCharArray() : null);
+
+      if (alias == null) {
+         return fact.getKeyManagers();
+      }
+      else {
+         validateAlias(keyStore, alias);
+         return wrapKeyManagers(alias, fact.getKeyManagers());
+      }
+   }
+
+   private static KeyManager[] wrapKeyManagers(String alias, KeyManager[] origKeyManagers) {
+      KeyManager[] keyManagers = new KeyManager[origKeyManagers.length];
+      for (int i = 0; i < origKeyManagers.length; i++) {
+         KeyManager km = origKeyManagers[i];
+         if (km instanceof X509ExtendedKeyManager) {
+            km = new X509AliasKeyManager(alias, (X509ExtendedKeyManager) km);
+         }
+
+         keyManagers[i] = km;
+      }
+
+      return keyManagers;
+   }
+
+   private static void validateAlias(KeyStore store, String alias) throws IllegalArgumentException, KeyStoreException {
+      if (!store.containsAlias(alias)) {
+         throw new IllegalArgumentException("The alias '" + alias + "' doesn't exist in the key store");
+      }
+
+      if (!store.isKeyEntry(alias)) {
+         throw new IllegalArgumentException("The alias '" + alias + "' in the keystore doesn't represent a key entry");
+      }
+   }
+
+   private static KeyStore loadStore(String storePath, final String password, String storeType) throws Exception {
+      KeyStore store = KeyStore.getInstance(storeType);
+      try (InputStream in = new FileInputStream(new File(storePath));) {
+         store.load(in, password != null ? password.toCharArray() : null);
+      }
+
+      return store;
+   }
+
+   private static TrustManager createTrustAllTrustManager() {
+      return new X509TrustManager() {
+         @Override
+         public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+         }
+
+         @Override
+         public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+         }
+
+         @Override
+         public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+         }
+      };
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyWSTransport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyWSTransport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyWSTransport.java
new file mode 100644
index 0000000..b28f523
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/NettyWSTransport.java
@@ -0,0 +1,472 @@
+/*
+ * 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.activemq.transport.amqp.client.transport;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.FixedRecvByteBufAllocator;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketVersion;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.CharsetUtil;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import org.apache.activemq.transport.amqp.client.util.IOExceptionSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Transport for communicating over WebSockets
+ */
+public class NettyWSTransport implements NettyTransport {
+
+   private static final Logger LOG = LoggerFactory.getLogger(NettyWSTransport.class);
+
+   private static final int QUIET_PERIOD = 20;
+   private static final int SHUTDOWN_TIMEOUT = 100;
+
+   protected Bootstrap bootstrap;
+   protected EventLoopGroup group;
+   protected Channel channel;
+   protected NettyTransportListener listener;
+   protected NettyTransportOptions options;
+   protected final URI remote;
+   protected boolean secure;
+
+   private final AtomicBoolean connected = new AtomicBoolean();
+   private final AtomicBoolean closed = new AtomicBoolean();
+   private ChannelPromise handshakeFuture;
+   private IOException failureCause;
+   private Throwable pendingFailure;
+
+   /**
+    * Create a new transport instance
+    *
+    * @param remoteLocation the URI that defines the remote resource to connect to.
+    * @param options        the transport options used to configure the socket connection.
+    */
+   public NettyWSTransport(URI remoteLocation, NettyTransportOptions options) {
+      this(null, remoteLocation, options);
+   }
+
+   /**
+    * Create a new transport instance
+    *
+    * @param listener       the TransportListener that will receive events from this Transport.
+    * @param remoteLocation the URI that defines the remote resource to connect to.
+    * @param options        the transport options used to configure the socket connection.
+    */
+   public NettyWSTransport(NettyTransportListener listener, URI remoteLocation, NettyTransportOptions options) {
+      this.options = options;
+      this.listener = listener;
+      this.remote = remoteLocation;
+      this.secure = remoteLocation.getScheme().equalsIgnoreCase("wss");
+   }
+
+   @Override
+   public void connect() throws IOException {
+
+      if (listener == null) {
+         throw new IllegalStateException("A transport listener must be set before connection attempts.");
+      }
+
+      group = new NioEventLoopGroup(1);
+
+      bootstrap = new Bootstrap();
+      bootstrap.group(group);
+      bootstrap.channel(NioSocketChannel.class);
+      bootstrap.handler(new ChannelInitializer<Channel>() {
+
+         @Override
+         public void initChannel(Channel connectedChannel) throws Exception {
+            configureChannel(connectedChannel);
+         }
+      });
+
+      configureNetty(bootstrap, getTransportOptions());
+
+      ChannelFuture future;
+      try {
+         future = bootstrap.connect(getRemoteHost(), getRemotePort());
+         future.addListener(new ChannelFutureListener() {
+
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+               if (future.isSuccess()) {
+                  handleConnected(future.channel());
+               }
+               else if (future.isCancelled()) {
+                  connectionFailed(future.channel(), new IOException("Connection attempt was cancelled"));
+               }
+               else {
+                  connectionFailed(future.channel(), IOExceptionSupport.create(future.cause()));
+               }
+            }
+         });
+
+         future.sync();
+
+         // Now wait for WS protocol level handshake completion
+         handshakeFuture.await();
+      }
+      catch (InterruptedException ex) {
+         LOG.debug("Transport connection attempt was interrupted.");
+         Thread.interrupted();
+         failureCause = IOExceptionSupport.create(ex);
+      }
+
+      if (failureCause != null) {
+         // Close out any Netty resources now as they are no longer needed.
+         if (channel != null) {
+            channel.close().syncUninterruptibly();
+            channel = null;
+         }
+         if (group != null) {
+            group.shutdownGracefully(QUIET_PERIOD, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
+            group = null;
+         }
+
+         throw failureCause;
+      }
+      else {
+         // Connected, allow any held async error to fire now and close the transport.
+         channel.eventLoop().execute(new Runnable() {
+
+            @Override
+            public void run() {
+               if (pendingFailure != null) {
+                  channel.pipeline().fireExceptionCaught(pendingFailure);
+               }
+            }
+         });
+      }
+   }
+
+   @Override
+   public boolean isConnected() {
+      return connected.get();
+   }
+
+   @Override
+   public boolean isSSL() {
+      return secure;
+   }
+
+   @Override
+   public void close() throws IOException {
+      if (closed.compareAndSet(false, true)) {
+         connected.set(false);
+         if (channel != null) {
+            channel.close().syncUninterruptibly();
+         }
+         if (group != null) {
+            group.shutdownGracefully(QUIET_PERIOD, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
+         }
+      }
+   }
+
+   @Override
+   public ByteBuf allocateSendBuffer(int size) throws IOException {
+      checkConnected();
+      return channel.alloc().ioBuffer(size, size);
+   }
+
+   @Override
+   public void send(ByteBuf output) throws IOException {
+      checkConnected();
+      int length = output.readableBytes();
+      if (length == 0) {
+         return;
+      }
+
+      LOG.trace("Attempted write of: {} bytes", length);
+
+      channel.writeAndFlush(new BinaryWebSocketFrame(output));
+   }
+
+   @Override
+   public NettyTransportListener getTransportListener() {
+      return listener;
+   }
+
+   @Override
+   public void setTransportListener(NettyTransportListener listener) {
+      this.listener = listener;
+   }
+
+   @Override
+   public NettyTransportOptions getTransportOptions() {
+      if (options == null) {
+         if (isSSL()) {
+            options = NettyTransportSslOptions.INSTANCE;
+         }
+         else {
+            options = NettyTransportOptions.INSTANCE;
+         }
+      }
+
+      return options;
+   }
+
+   @Override
+   public URI getRemoteLocation() {
+      return remote;
+   }
+
+   @Override
+   public Principal getLocalPrincipal() {
+      if (!isSSL()) {
+         throw new UnsupportedOperationException("Not connected to a secure channel");
+      }
+
+      SslHandler sslHandler = channel.pipeline().get(SslHandler.class);
+
+      return sslHandler.engine().getSession().getLocalPrincipal();
+   }
+
+   //----- Internal implementation details, can be overridden as needed --//
+
+   protected String getRemoteHost() {
+      return remote.getHost();
+   }
+
+   protected int getRemotePort() {
+      int port = remote.getPort();
+
+      if (port <= 0) {
+         if (isSSL()) {
+            port = getSslOptions().getDefaultSslPort();
+         }
+         else {
+            port = getTransportOptions().getDefaultTcpPort();
+         }
+      }
+
+      return port;
+   }
+
+   protected void configureNetty(Bootstrap bootstrap, NettyTransportOptions options) {
+      bootstrap.option(ChannelOption.TCP_NODELAY, options.isTcpNoDelay());
+      bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, options.getConnectTimeout());
+      bootstrap.option(ChannelOption.SO_KEEPALIVE, options.isTcpKeepAlive());
+      bootstrap.option(ChannelOption.SO_LINGER, options.getSoLinger());
+      bootstrap.option(ChannelOption.ALLOCATOR, PartialPooledByteBufAllocator.INSTANCE);
+
+      if (options.getSendBufferSize() != -1) {
+         bootstrap.option(ChannelOption.SO_SNDBUF, options.getSendBufferSize());
+      }
+
+      if (options.getReceiveBufferSize() != -1) {
+         bootstrap.option(ChannelOption.SO_RCVBUF, options.getReceiveBufferSize());
+         bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(options.getReceiveBufferSize()));
+      }
+
+      if (options.getTrafficClass() != -1) {
+         bootstrap.option(ChannelOption.IP_TOS, options.getTrafficClass());
+      }
+   }
+
+   protected void configureChannel(final Channel channel) throws Exception {
+      if (isSSL()) {
+         SslHandler sslHandler = NettyTransportSupport.createSslHandler(getRemoteLocation(), getSslOptions());
+         sslHandler.handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {
+            @Override
+            public void operationComplete(Future<Channel> future) throws Exception {
+               if (future.isSuccess()) {
+                  LOG.trace("SSL Handshake has completed: {}", channel);
+                  connectionEstablished(channel);
+               }
+               else {
+                  LOG.trace("SSL Handshake has failed: {}", channel);
+                  connectionFailed(channel, IOExceptionSupport.create(future.cause()));
+               }
+            }
+         });
+
+         channel.pipeline().addLast(sslHandler);
+      }
+
+      channel.pipeline().addLast(new HttpClientCodec());
+      channel.pipeline().addLast(new HttpObjectAggregator(8192));
+      channel.pipeline().addLast(new NettyTcpTransportHandler());
+   }
+
+   protected void handleConnected(final Channel channel) throws Exception {
+      if (!isSSL()) {
+         connectionEstablished(channel);
+      }
+   }
+
+   //----- State change handlers and checks ---------------------------------//
+
+   /**
+    * Called when the transport has successfully connected and is ready for use.
+    */
+   protected void connectionEstablished(Channel connectedChannel) {
+      LOG.info("WebSocket connectionEstablished! {}", connectedChannel);
+      channel = connectedChannel;
+      connected.set(true);
+   }
+
+   /**
+    * Called when the transport connection failed and an error should be returned.
+    *
+    * @param failedChannel The Channel instance that failed.
+    * @param cause         An IOException that describes the cause of the failed connection.
+    */
+   protected void connectionFailed(Channel failedChannel, IOException cause) {
+      failureCause = IOExceptionSupport.create(cause);
+      channel = failedChannel;
+      connected.set(false);
+      handshakeFuture.setFailure(cause);
+   }
+
+   private NettyTransportSslOptions getSslOptions() {
+      return (NettyTransportSslOptions) getTransportOptions();
+   }
+
+   private void checkConnected() throws IOException {
+      if (!connected.get()) {
+         throw new IOException("Cannot send to a non-connected transport.");
+      }
+   }
+
+   //----- Handle connection events -----------------------------------------//
+
+   private class NettyTcpTransportHandler extends SimpleChannelInboundHandler<Object> {
+
+      private final WebSocketClientHandshaker handshaker;
+
+      NettyTcpTransportHandler() {
+         handshaker = WebSocketClientHandshakerFactory.newHandshaker(remote, WebSocketVersion.V13, "amqp", false, new DefaultHttpHeaders());
+      }
+
+      @Override
+      public void handlerAdded(ChannelHandlerContext context) {
+         LOG.trace("Handler has become added! Channel is {}", context.channel());
+         handshakeFuture = context.newPromise();
+      }
+
+      @Override
+      public void channelActive(ChannelHandlerContext context) throws Exception {
+         LOG.trace("Channel has become active! Channel is {}", context.channel());
+         handshaker.handshake(context.channel());
+      }
+
+      @Override
+      public void channelInactive(ChannelHandlerContext context) throws Exception {
+         LOG.trace("Channel has gone inactive! Channel is {}", context.channel());
+         if (connected.compareAndSet(true, false) && !closed.get()) {
+            LOG.trace("Firing onTransportClosed listener");
+            listener.onTransportClosed();
+         }
+      }
+
+      @Override
+      public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
+         LOG.trace("Exception on channel! Channel is {} -> {}", context.channel(), cause.getMessage());
+         LOG.trace("Error Stack: ", cause);
+         if (connected.compareAndSet(true, false) && !closed.get()) {
+            LOG.trace("Firing onTransportError listener");
+            if (pendingFailure != null) {
+               listener.onTransportError(pendingFailure);
+            }
+            else {
+               listener.onTransportError(cause);
+            }
+         }
+         else {
+            // Hold the first failure for later dispatch if connect succeeds.
+            // This will then trigger disconnect using the first error reported.
+            if (pendingFailure != null) {
+               LOG.trace("Holding error until connect succeeds: {}", cause.getMessage());
+               pendingFailure = cause;
+            }
+
+            if (!handshakeFuture.isDone()) {
+               handshakeFuture.setFailure(cause);
+            }
+         }
+      }
+
+      @Override
+      protected void channelRead0(ChannelHandlerContext ctx, Object message) throws Exception {
+         LOG.trace("New data read: incoming: {}", message);
+
+         Channel ch = ctx.channel();
+         if (!handshaker.isHandshakeComplete()) {
+            handshaker.finishHandshake(ch, (FullHttpResponse) message);
+            LOG.info("WebSocket Client connected! {}", ctx.channel());
+            handshakeFuture.setSuccess();
+            return;
+         }
+
+         // We shouldn't get this since we handle the handshake previously.
+         if (message instanceof FullHttpResponse) {
+            FullHttpResponse response = (FullHttpResponse) message;
+            throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.getStatus() +
+                                               ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
+         }
+
+         WebSocketFrame frame = (WebSocketFrame) message;
+         if (frame instanceof TextWebSocketFrame) {
+            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
+            LOG.warn("WebSocket Client received message: " + textFrame.text());
+            ctx.fireExceptionCaught(new IOException("Received invalid frame over WebSocket."));
+         }
+         else if (frame instanceof BinaryWebSocketFrame) {
+            BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame;
+            LOG.info("WebSocket Client received data: {} bytes", binaryFrame.content().readableBytes());
+            listener.onData(binaryFrame.content());
+         }
+         else if (frame instanceof PongWebSocketFrame) {
+            LOG.trace("WebSocket Client received pong");
+         }
+         else if (frame instanceof CloseWebSocketFrame) {
+            LOG.trace("WebSocket Client received closing");
+            ch.close();
+         }
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/PartialPooledByteBufAllocator.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/PartialPooledByteBufAllocator.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/PartialPooledByteBufAllocator.java
new file mode 100644
index 0000000..c3c4286
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/PartialPooledByteBufAllocator.java
@@ -0,0 +1,134 @@
+/*
+ * 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.activemq.transport.amqp.client.transport;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.CompositeByteBuf;
+import io.netty.buffer.PooledByteBufAllocator;
+import io.netty.buffer.UnpooledByteBufAllocator;
+
+/**
+ * A {@link ByteBufAllocator} which is partial pooled. Which means only direct
+ * {@link ByteBuf}s are pooled. The rest is unpooled.
+ *
+ */
+public class PartialPooledByteBufAllocator implements ByteBufAllocator {
+
+   private static final ByteBufAllocator POOLED = new PooledByteBufAllocator(false);
+   private static final ByteBufAllocator UNPOOLED = new UnpooledByteBufAllocator(false);
+
+   public static final PartialPooledByteBufAllocator INSTANCE = new PartialPooledByteBufAllocator();
+
+   private PartialPooledByteBufAllocator() {
+   }
+
+   @Override
+   public ByteBuf buffer() {
+      return UNPOOLED.heapBuffer();
+   }
+
+   @Override
+   public ByteBuf buffer(int initialCapacity) {
+      return UNPOOLED.heapBuffer(initialCapacity);
+   }
+
+   @Override
+   public ByteBuf buffer(int initialCapacity, int maxCapacity) {
+      return UNPOOLED.heapBuffer(initialCapacity, maxCapacity);
+   }
+
+   @Override
+   public ByteBuf ioBuffer() {
+      return UNPOOLED.heapBuffer();
+   }
+
+   @Override
+   public ByteBuf ioBuffer(int initialCapacity) {
+      return UNPOOLED.heapBuffer(initialCapacity);
+   }
+
+   @Override
+   public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) {
+      return UNPOOLED.heapBuffer(initialCapacity, maxCapacity);
+   }
+
+   @Override
+   public ByteBuf heapBuffer() {
+      return UNPOOLED.heapBuffer();
+   }
+
+   @Override
+   public ByteBuf heapBuffer(int initialCapacity) {
+      return UNPOOLED.heapBuffer(initialCapacity);
+   }
+
+   @Override
+   public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) {
+      return UNPOOLED.heapBuffer(initialCapacity, maxCapacity);
+   }
+
+   @Override
+   public ByteBuf directBuffer() {
+      return POOLED.directBuffer();
+   }
+
+   @Override
+   public ByteBuf directBuffer(int initialCapacity) {
+      return POOLED.directBuffer(initialCapacity);
+   }
+
+   @Override
+   public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
+      return POOLED.directBuffer(initialCapacity, maxCapacity);
+   }
+
+   @Override
+   public CompositeByteBuf compositeBuffer() {
+      return UNPOOLED.compositeHeapBuffer();
+   }
+
+   @Override
+   public CompositeByteBuf compositeBuffer(int maxNumComponents) {
+      return UNPOOLED.compositeHeapBuffer(maxNumComponents);
+   }
+
+   @Override
+   public CompositeByteBuf compositeHeapBuffer() {
+      return UNPOOLED.compositeHeapBuffer();
+   }
+
+   @Override
+   public CompositeByteBuf compositeHeapBuffer(int maxNumComponents) {
+      return UNPOOLED.compositeHeapBuffer(maxNumComponents);
+   }
+
+   @Override
+   public CompositeByteBuf compositeDirectBuffer() {
+      return POOLED.compositeDirectBuffer();
+   }
+
+   @Override
+   public CompositeByteBuf compositeDirectBuffer(int maxNumComponents) {
+      return POOLED.compositeDirectBuffer();
+   }
+
+   @Override
+   public boolean isDirectBufferPooled() {
+      return true;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/X509AliasKeyManager.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/X509AliasKeyManager.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/X509AliasKeyManager.java
new file mode 100644
index 0000000..42d6a0b
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/transport/X509AliasKeyManager.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.activemq.transport.amqp.client.transport;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+/**
+ * An X509ExtendedKeyManager wrapper which always chooses and only
+ * returns the given alias, and defers retrieval to the delegate
+ * key manager.
+ */
+public class X509AliasKeyManager extends X509ExtendedKeyManager {
+
+   private X509ExtendedKeyManager delegate;
+   private String alias;
+
+   public X509AliasKeyManager(String alias, X509ExtendedKeyManager delegate) throws IllegalArgumentException {
+      if (alias == null) {
+         throw new IllegalArgumentException("The given key alias must not be null.");
+      }
+
+      this.alias = alias;
+      this.delegate = delegate;
+   }
+
+   @Override
+   public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+      return alias;
+   }
+
+   @Override
+   public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+      return alias;
+   }
+
+   @Override
+   public X509Certificate[] getCertificateChain(String alias) {
+      return delegate.getCertificateChain(alias);
+   }
+
+   @Override
+   public String[] getClientAliases(String keyType, Principal[] issuers) {
+      return new String[]{alias};
+   }
+
+   @Override
+   public PrivateKey getPrivateKey(String alias) {
+      return delegate.getPrivateKey(alias);
+   }
+
+   @Override
+   public String[] getServerAliases(String keyType, Principal[] issuers) {
+      return new String[]{alias};
+   }
+
+   @Override
+   public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+      return alias;
+   }
+
+   @Override
+   public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+      return alias;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/AsyncResult.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/AsyncResult.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/AsyncResult.java
new file mode 100644
index 0000000..bb71746
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/AsyncResult.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.activemq.transport.amqp.client.util;
+
+/**
+ * Defines a result interface for Asynchronous operations.
+ */
+public interface AsyncResult {
+
+   /**
+    * If the operation fails this method is invoked with the Exception
+    * that caused the failure.
+    *
+    * @param result The error that resulted in this asynchronous operation failing.
+    */
+   void onFailure(Throwable result);
+
+   /**
+    * If the operation succeeds the resulting value produced is set to null and
+    * the waiting parties are signaled.
+    */
+   void onSuccess();
+
+   /**
+    * Returns true if the AsyncResult has completed.  The task is considered complete
+    * regardless if it succeeded or failed.
+    *
+    * @return returns true if the asynchronous operation has completed.
+    */
+   boolean isComplete();
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFuture.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFuture.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFuture.java
new file mode 100644
index 0000000..12d38fd
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFuture.java
@@ -0,0 +1,110 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Asynchronous Client Future class.
+ */
+public class ClientFuture implements AsyncResult {
+
+   private final AtomicBoolean completer = new AtomicBoolean();
+   private final CountDownLatch latch = new CountDownLatch(1);
+   private final ClientFutureSynchronization synchronization;
+   private volatile Throwable error;
+
+   public ClientFuture() {
+      this(null);
+   }
+
+   public ClientFuture(ClientFutureSynchronization synchronization) {
+      this.synchronization = synchronization;
+   }
+
+   @Override
+   public boolean isComplete() {
+      return latch.getCount() == 0;
+   }
+
+   @Override
+   public void onFailure(Throwable result) {
+      if (completer.compareAndSet(false, true)) {
+         error = result;
+         if (synchronization != null) {
+            synchronization.onPendingFailure(error);
+         }
+         latch.countDown();
+      }
+   }
+
+   @Override
+   public void onSuccess() {
+      if (completer.compareAndSet(false, true)) {
+         if (synchronization != null) {
+            synchronization.onPendingSuccess();
+         }
+         latch.countDown();
+      }
+   }
+
+   /**
+    * Timed wait for a response to a pending operation.
+    *
+    * @param amount The amount of time to wait before abandoning the wait.
+    * @param unit   The unit to use for this wait period.
+    * @throws IOException if an error occurs while waiting for the response.
+    */
+   public void sync(long amount, TimeUnit unit) throws IOException {
+      try {
+         latch.await(amount, unit);
+      }
+      catch (InterruptedException e) {
+         Thread.interrupted();
+         throw IOExceptionSupport.create(e);
+      }
+
+      failOnError();
+   }
+
+   /**
+    * Waits for a response to some pending operation.
+    *
+    * @throws IOException if an error occurs while waiting for the response.
+    */
+   public void sync() throws IOException {
+      try {
+         latch.await();
+      }
+      catch (InterruptedException e) {
+         Thread.interrupted();
+         throw IOExceptionSupport.create(e);
+      }
+
+      failOnError();
+   }
+
+   private void failOnError() throws IOException {
+      Throwable cause = error;
+      if (cause != null) {
+         throw IOExceptionSupport.create(cause);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFutureSynchronization.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFutureSynchronization.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFutureSynchronization.java
new file mode 100644
index 0000000..e279bc1
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/ClientFutureSynchronization.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.activemq.transport.amqp.client.util;
+
+/**
+ * Synchronization callback interface used to execute state updates
+ * or similar tasks in the thread context where the associated
+ * ProviderFuture is managed.
+ */
+public interface ClientFutureSynchronization {
+
+   void onPendingSuccess();
+
+   void onPendingFailure(Throwable cause);
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IOExceptionSupport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IOExceptionSupport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IOExceptionSupport.java
new file mode 100644
index 0000000..70d88e6
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IOExceptionSupport.java
@@ -0,0 +1,45 @@
+/*
+ * 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.activemq.transport.amqp.client.util;
+
+import java.io.IOException;
+
+/**
+ * Used to make throwing IOException instances easier.
+ */
+public class IOExceptionSupport {
+
+   /**
+    * Checks the given cause to determine if it's already an IOException type and
+    * if not creates a new IOException to wrap it.
+    *
+    * @param cause The initiating exception that should be cast or wrapped.
+    * @return an IOException instance.
+    */
+   public static IOException create(Throwable cause) {
+      if (cause instanceof IOException) {
+         return (IOException) cause;
+      }
+
+      String message = cause.getMessage();
+      if (message == null || message.length() == 0) {
+         message = cause.toString();
+      }
+
+      return new IOException(message, cause);
+   }
+}


[5/9] activemq-artemis git commit: ARTEMIS-637 Port 5.x AMQP test client

Posted by an...@apache.org.
http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IdGenerator.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IdGenerator.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IdGenerator.java
new file mode 100644
index 0000000..c662b59
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/IdGenerator.java
@@ -0,0 +1,274 @@
+/*
+ * 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.activemq.transport.amqp.client.util;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.UnknownHostException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Generator for Globally unique Strings.
+ */
+public class IdGenerator {
+
+   private static final Logger LOG = LoggerFactory.getLogger(IdGenerator.class);
+   private static final String UNIQUE_STUB;
+   private static int instanceCount;
+   private static String hostName;
+   private String seed;
+   private final AtomicLong sequence = new AtomicLong(1);
+   private int length;
+   public static final String PROPERTY_IDGENERATOR_PORT = "activemq.idgenerator.port";
+
+   static {
+      String stub = "";
+      boolean canAccessSystemProps = true;
+      try {
+         SecurityManager sm = System.getSecurityManager();
+         if (sm != null) {
+            sm.checkPropertiesAccess();
+         }
+      }
+      catch (SecurityException se) {
+         canAccessSystemProps = false;
+      }
+
+      if (canAccessSystemProps) {
+         int idGeneratorPort = 0;
+         ServerSocket ss = null;
+         try {
+            idGeneratorPort = Integer.parseInt(System.getProperty(PROPERTY_IDGENERATOR_PORT, "0"));
+            LOG.trace("Using port {}", idGeneratorPort);
+            hostName = getLocalHostName();
+            ss = new ServerSocket(idGeneratorPort);
+            stub = "-" + ss.getLocalPort() + "-" + System.currentTimeMillis() + "-";
+            Thread.sleep(100);
+         }
+         catch (Exception e) {
+            if (LOG.isTraceEnabled()) {
+               LOG.trace("could not generate unique stub by using DNS and binding to local port", e);
+            }
+            else {
+               LOG.warn("could not generate unique stub by using DNS and binding to local port: {} {}", e.getClass().getCanonicalName(), e.getMessage());
+            }
+
+            // Restore interrupted state so higher level code can deal with it.
+            if (e instanceof InterruptedException) {
+               Thread.currentThread().interrupt();
+            }
+         }
+         finally {
+            if (ss != null) {
+               try {
+                  ss.close();
+               }
+               catch (IOException ioe) {
+                  if (LOG.isTraceEnabled()) {
+                     LOG.trace("Closing the server socket failed", ioe);
+                  }
+                  else {
+                     LOG.warn("Closing the server socket failed" + " due " + ioe.getMessage());
+                  }
+               }
+            }
+         }
+      }
+
+      if (hostName == null) {
+         hostName = "localhost";
+      }
+      hostName = sanitizeHostName(hostName);
+
+      if (stub.length() == 0) {
+         stub = "-1-" + System.currentTimeMillis() + "-";
+      }
+      UNIQUE_STUB = stub;
+   }
+
+   /**
+    * Construct an IdGenerator
+    *
+    * @param prefix The prefix value that is applied to all generated IDs.
+    */
+   public IdGenerator(String prefix) {
+      synchronized (UNIQUE_STUB) {
+         this.seed = prefix + UNIQUE_STUB + (instanceCount++) + ":";
+         this.length = this.seed.length() + ("" + Long.MAX_VALUE).length();
+      }
+   }
+
+   public IdGenerator() {
+      this("ID:" + hostName);
+   }
+
+   /**
+    * As we have to find the host name as a side-affect of generating a unique stub, we allow
+    * it's easy retrieval here
+    *
+    * @return the local host name
+    */
+   public static String getHostName() {
+      return hostName;
+   }
+
+   /**
+    * Generate a unique id
+    *
+    * @return a unique id
+    */
+   public synchronized String generateId() {
+      StringBuilder sb = new StringBuilder(length);
+      sb.append(seed);
+      sb.append(sequence.getAndIncrement());
+      return sb.toString();
+   }
+
+   public static String sanitizeHostName(String hostName) {
+      boolean changed = false;
+
+      StringBuilder sb = new StringBuilder();
+      for (char ch : hostName.toCharArray()) {
+         // only include ASCII chars
+         if (ch < 127) {
+            sb.append(ch);
+         }
+         else {
+            changed = true;
+         }
+      }
+
+      if (changed) {
+         String newHost = sb.toString();
+         LOG.info("Sanitized hostname from: {} to: {}", hostName, newHost);
+         return newHost;
+      }
+      else {
+         return hostName;
+      }
+   }
+
+   /**
+    * Generate a unique ID - that is friendly for a URL or file system
+    *
+    * @return a unique id
+    */
+   public String generateSanitizedId() {
+      String result = generateId();
+      result = result.replace(':', '-');
+      result = result.replace('_', '-');
+      result = result.replace('.', '-');
+      return result;
+   }
+
+   /**
+    * From a generated id - return the seed (i.e. minus the count)
+    *
+    * @param id the generated identifier
+    * @return the seed
+    */
+   public static String getSeedFromId(String id) {
+      String result = id;
+      if (id != null) {
+         int index = id.lastIndexOf(':');
+         if (index > 0 && (index + 1) < id.length()) {
+            result = id.substring(0, index);
+         }
+      }
+      return result;
+   }
+
+   /**
+    * From a generated id - return the generator count
+    *
+    * @param id The ID that will be parsed for a sequence number.
+    * @return the sequence value parsed from the given ID.
+    */
+   public static long getSequenceFromId(String id) {
+      long result = -1;
+      if (id != null) {
+         int index = id.lastIndexOf(':');
+
+         if (index > 0 && (index + 1) < id.length()) {
+            String numStr = id.substring(index + 1, id.length());
+            result = Long.parseLong(numStr);
+         }
+      }
+      return result;
+   }
+
+   /**
+    * Does a proper compare on the Id's
+    *
+    * @param id1 the lhs of the comparison.
+    * @param id2 the rhs of the comparison.
+    * @return 0 if equal else a positive if {@literal id1 > id2} ...
+    */
+   public static int compare(String id1, String id2) {
+      int result = -1;
+      String seed1 = IdGenerator.getSeedFromId(id1);
+      String seed2 = IdGenerator.getSeedFromId(id2);
+      if (seed1 != null && seed2 != null) {
+         result = seed1.compareTo(seed2);
+         if (result == 0) {
+            long count1 = IdGenerator.getSequenceFromId(id1);
+            long count2 = IdGenerator.getSequenceFromId(id2);
+            result = (int) (count1 - count2);
+         }
+      }
+      return result;
+   }
+
+   /**
+    * When using the {@link java.net.InetAddress#getHostName()} method in an
+    * environment where neither a proper DNS lookup nor an <tt>/etc/hosts</tt>
+    * entry exists for a given host, the following exception will be thrown:
+    * <code>
+    * java.net.UnknownHostException: &lt;hostname&gt;: &lt;hostname&gt;
+    * at java.net.InetAddress.getLocalHost(InetAddress.java:1425)
+    * ...
+    * </code>
+    * Instead of just throwing an UnknownHostException and giving up, this
+    * method grabs a suitable hostname from the exception and prevents the
+    * exception from being thrown. If a suitable hostname cannot be acquired
+    * from the exception, only then is the <tt>UnknownHostException</tt> thrown.
+    *
+    * @return The hostname
+    * @throws UnknownHostException if the given host cannot be looked up.
+    * @see java.net.InetAddress#getLocalHost()
+    * @see java.net.InetAddress#getHostName()
+    */
+   protected static String getLocalHostName() throws UnknownHostException {
+      try {
+         return (InetAddress.getLocalHost()).getHostName();
+      }
+      catch (UnknownHostException uhe) {
+         String host = uhe.getMessage(); // host = "hostname: hostname"
+         if (host != null) {
+            int colon = host.indexOf(':');
+            if (colon > 0) {
+               return host.substring(0, colon);
+            }
+         }
+         throw uhe;
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/NoOpAsyncResult.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/NoOpAsyncResult.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/NoOpAsyncResult.java
new file mode 100644
index 0000000..5dd4d12
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/NoOpAsyncResult.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.activemq.transport.amqp.client.util;
+
+/**
+ * Simple NoOp implementation used when the result of the operation does not matter.
+ */
+public class NoOpAsyncResult implements AsyncResult {
+
+   public static final NoOpAsyncResult INSTANCE = new NoOpAsyncResult();
+
+   @Override
+   public void onFailure(Throwable result) {
+
+   }
+
+   @Override
+   public void onSuccess() {
+
+   }
+
+   @Override
+   public boolean isComplete() {
+      return true;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/PropertyUtil.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/PropertyUtil.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/PropertyUtil.java
new file mode 100644
index 0000000..1285a0f
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/PropertyUtil.java
@@ -0,0 +1,533 @@
+/*
+ * 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.activemq.transport.amqp.client.util;
+
+import javax.net.ssl.SSLContext;
+import java.beans.BeanInfo;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+
+/**
+ * Utilities for properties
+ */
+public class PropertyUtil {
+
+   /**
+    * Creates a URI from the original URI and the given parameters.
+    *
+    * @param originalURI The URI whose current parameters are removed and replaced with the given remainder value.
+    * @param params      The URI params that should be used to replace the current ones in the target.
+    * @return a new URI that matches the original one but has its query options replaced with
+    * the given ones.
+    * @throws URISyntaxException if the given URI is invalid.
+    */
+   public static URI replaceQuery(URI originalURI, Map<String, String> params) throws URISyntaxException {
+      String s = createQueryString(params);
+      if (s.length() == 0) {
+         s = null;
+      }
+      return replaceQuery(originalURI, s);
+   }
+
+   /**
+    * Creates a URI with the given query, removing an previous query value from the given URI.
+    *
+    * @param uri   The source URI whose existing query is replaced with the newly supplied one.
+    * @param query The new URI query string that should be appended to the given URI.
+    * @return a new URI that is a combination of the original URI and the given query string.
+    * @throws URISyntaxException if the given URI is invalid.
+    */
+   public static URI replaceQuery(URI uri, String query) throws URISyntaxException {
+      String schemeSpecificPart = uri.getRawSchemeSpecificPart();
+      // strip existing query if any
+      int questionMark = schemeSpecificPart.lastIndexOf("?");
+      // make sure question mark is not within parentheses
+      if (questionMark < schemeSpecificPart.lastIndexOf(")")) {
+         questionMark = -1;
+      }
+      if (questionMark > 0) {
+         schemeSpecificPart = schemeSpecificPart.substring(0, questionMark);
+      }
+      if (query != null && query.length() > 0) {
+         schemeSpecificPart += "?" + query;
+      }
+      return new URI(uri.getScheme(), schemeSpecificPart, uri.getFragment());
+   }
+
+   /**
+    * Creates a URI with the given query, removing an previous query value from the given URI.
+    *
+    * @param uri The source URI whose existing query is replaced with the newly supplied one.
+    * @return a new URI that is a combination of the original URI and the given query string.
+    * @throws URISyntaxException if the given URI is invalid.
+    */
+   public static URI eraseQuery(URI uri) throws URISyntaxException {
+      return replaceQuery(uri, (String) null);
+   }
+
+   /**
+    * Given a key / value mapping, create and return a URI formatted query string that is valid
+    * and can be appended to a URI.
+    *
+    * @param options The Mapping that will create the new Query string.
+    * @return a URI formatted query string.
+    * @throws URISyntaxException if the given URI is invalid.
+    */
+   public static String createQueryString(Map<String, ?> options) throws URISyntaxException {
+      try {
+         if (options.size() > 0) {
+            StringBuffer rc = new StringBuffer();
+            boolean first = true;
+            for (Entry<String, ?> entry : options.entrySet()) {
+               if (first) {
+                  first = false;
+               }
+               else {
+                  rc.append("&");
+               }
+               rc.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
+               rc.append("=");
+               rc.append(URLEncoder.encode((String) entry.getValue(), "UTF-8"));
+            }
+            return rc.toString();
+         }
+         else {
+            return "";
+         }
+      }
+      catch (UnsupportedEncodingException e) {
+         throw (URISyntaxException) new URISyntaxException(e.toString(), "Invalid encoding").initCause(e);
+      }
+   }
+
+   /**
+    * Get properties from a URI and return them in a new {@code Map<String, String>} instance.
+    *
+    * If the URI is null or the query string of the URI is null an empty Map is returned.
+    *
+    * @param uri the URI whose parameters are to be parsed.
+    * @return <Code>Map</Code> of properties
+    * @throws Exception if an error occurs while parsing the query options.
+    */
+   public static Map<String, String> parseParameters(URI uri) throws Exception {
+      if (uri == null || uri.getQuery() == null) {
+         return Collections.emptyMap();
+      }
+
+      return parseQuery(stripPrefix(uri.getQuery(), "?"));
+   }
+
+   /**
+    * Parse properties from a named resource -eg. a URI or a simple name e.g.
+    * {@literal foo?name="fred"&size=2}
+    *
+    * @param uri the URI whose parameters are to be parsed.
+    * @return <Code>Map</Code> of properties
+    * @throws Exception if an error occurs while parsing the query options.
+    */
+   public static Map<String, String> parseParameters(String uri) throws Exception {
+      if (uri == null) {
+         return Collections.emptyMap();
+      }
+
+      return parseQuery(stripUpto(uri, '?'));
+   }
+
+   /**
+    * Get properties from a URI query string.
+    *
+    * @param queryString the string value returned from a call to the URI class getQuery method.
+    * @return <Code>Map</Code> of properties from the parsed string.
+    * @throws Exception if an error occurs while parsing the query options.
+    */
+   public static Map<String, String> parseQuery(String queryString) throws Exception {
+      if (queryString != null && !queryString.isEmpty()) {
+         Map<String, String> rc = new HashMap<>();
+         String[] parameters = queryString.split("&");
+         for (int i = 0; i < parameters.length; i++) {
+            int p = parameters[i].indexOf("=");
+            if (p >= 0) {
+               String name = URLDecoder.decode(parameters[i].substring(0, p), "UTF-8");
+               String value = URLDecoder.decode(parameters[i].substring(p + 1), "UTF-8");
+               rc.put(name, value);
+            }
+            else {
+               rc.put(parameters[i], null);
+            }
+         }
+         return rc;
+      }
+
+      return Collections.emptyMap();
+   }
+
+   /**
+    * Given a map of properties, filter out only those prefixed with the given value, the
+    * values filtered are returned in a new Map instance.
+    *
+    * @param properties   The map of properties to filter.
+    * @param optionPrefix The prefix value to use when filtering.
+    * @return a filter map with only values that match the given prefix.
+    */
+   public static Map<String, String> filterProperties(Map<String, String> properties, String optionPrefix) {
+      if (properties == null) {
+         throw new IllegalArgumentException("The given properties object was null.");
+      }
+
+      HashMap<String, String> rc = new HashMap<>(properties.size());
+
+      for (Iterator<Entry<String, String>> iter = properties.entrySet().iterator(); iter.hasNext(); ) {
+         Entry<String, String> entry = iter.next();
+         if (entry.getKey().startsWith(optionPrefix)) {
+            String name = entry.getKey().substring(optionPrefix.length());
+            rc.put(name, entry.getValue());
+            iter.remove();
+         }
+      }
+
+      return rc;
+   }
+
+   /**
+    * Enumerate the properties of the target object and add them as additional entries
+    * to the query string of the given string URI.
+    *
+    * @param uri  The string URI value to append the object properties to.
+    * @param bean The Object whose properties will be added to the target URI.
+    * @return a new String value that is the original URI with the added bean properties.
+    * @throws Exception if an error occurs while enumerating the bean properties.
+    */
+   public static String addPropertiesToURIFromBean(String uri, Object bean) throws Exception {
+      Map<String, String> properties = PropertyUtil.getProperties(bean);
+      return PropertyUtil.addPropertiesToURI(uri, properties);
+   }
+
+   /**
+    * Enumerate the properties of the target object and add them as additional entries
+    * to the query string of the given URI.
+    *
+    * @param uri        The URI value to append the object properties to.
+    * @param properties The Object whose properties will be added to the target URI.
+    * @return a new String value that is the original URI with the added bean properties.
+    * @throws Exception if an error occurs while enumerating the bean properties.
+    */
+   public static String addPropertiesToURI(URI uri, Map<String, String> properties) throws Exception {
+      return addPropertiesToURI(uri.toString(), properties);
+   }
+
+   /**
+    * Append the given properties to the query portion of the given URI.
+    *
+    * @param uri        The string URI value to append the object properties to.
+    * @param properties The properties that will be added to the target URI.
+    * @return a new String value that is the original URI with the added properties.
+    * @throws Exception if an error occurs while building the new URI string.
+    */
+   public static String addPropertiesToURI(String uri, Map<String, String> properties) throws Exception {
+      String result = uri;
+      if (uri != null && properties != null) {
+         StringBuilder base = new StringBuilder(stripBefore(uri, '?'));
+         Map<String, String> map = parseParameters(uri);
+         if (!map.isEmpty()) {
+            map.putAll(properties);
+         }
+         else {
+            map = properties;
+         }
+         if (!map.isEmpty()) {
+            base.append('?');
+            boolean first = true;
+            for (Map.Entry<String, String> entry : map.entrySet()) {
+               if (!first) {
+                  base.append('&');
+               }
+               first = false;
+               base.append(entry.getKey()).append("=").append(entry.getValue());
+            }
+            result = base.toString();
+         }
+      }
+      return result;
+   }
+
+   /**
+    * Set properties on an object using the provided map. The return value
+    * indicates if all properties from the given map were set on the target object.
+    *
+    * @param target     the object whose properties are to be set from the map options.
+    * @param properties the properties that should be applied to the given object.
+    * @return true if all values in the properties map were applied to the target object.
+    */
+   public static Map<String, String> setProperties(Object target, Map<String, String> properties) {
+      if (target == null) {
+         throw new IllegalArgumentException("target object cannot be null");
+      }
+      if (properties == null) {
+         throw new IllegalArgumentException("Given Properties object cannot be null");
+      }
+
+      Map<String, String> unmatched = new HashMap<>();
+
+      for (Map.Entry<String, String> entry : properties.entrySet()) {
+         if (!setProperty(target, entry.getKey(), entry.getValue())) {
+            unmatched.put(entry.getKey(), entry.getValue());
+         }
+      }
+
+      return Collections.unmodifiableMap(unmatched);
+   }
+
+   //TODO: common impl for above and below methods.
+
+   /**
+    * Set properties on an object using the provided Properties object. The return value
+    * indicates if all properties from the given map were set on the target object.
+    *
+    * @param target     the object whose properties are to be set from the map options.
+    * @param properties the properties that should be applied to the given object.
+    * @return an unmodifiable map with any values that could not be applied to the target.
+    */
+   public static Map<String, Object> setProperties(Object target, Properties properties) {
+      if (target == null) {
+         throw new IllegalArgumentException("target object cannot be null");
+      }
+      if (properties == null) {
+         throw new IllegalArgumentException("Given Properties object cannot be null");
+      }
+
+      Map<String, Object> unmatched = new HashMap<>();
+
+      for (Map.Entry<Object, Object> entry : properties.entrySet()) {
+         if (!setProperty(target, (String) entry.getKey(), entry.getValue())) {
+            unmatched.put((String) entry.getKey(), entry.getValue());
+         }
+      }
+
+      return Collections.<String, Object>unmodifiableMap(unmatched);
+   }
+
+   /**
+    * Get properties from an object using reflection.  If the passed object is null an
+    * empty <code>Map</code> is returned.
+    *
+    * @param object the Object whose properties are to be extracted.
+    * @return <Code>Map</Code> of properties extracted from the given object.
+    * @throws Exception if an error occurs while examining the object's properties.
+    */
+   public static Map<String, String> getProperties(Object object) throws Exception {
+      if (object == null) {
+         return Collections.emptyMap();
+      }
+
+      Map<String, String> properties = new LinkedHashMap<>();
+      BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
+      Object[] NULL_ARG = {};
+      PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
+      if (propertyDescriptors != null) {
+         for (int i = 0; i < propertyDescriptors.length; i++) {
+            PropertyDescriptor pd = propertyDescriptors[i];
+            if (pd.getReadMethod() != null && !pd.getName().equals("class") && !pd.getName().equals("properties") && !pd.getName().equals("reference")) {
+               Object value = pd.getReadMethod().invoke(object, NULL_ARG);
+               if (value != null) {
+                  if (value instanceof Boolean || value instanceof Number || value instanceof String || value instanceof URI || value instanceof URL) {
+                     properties.put(pd.getName(), ("" + value));
+                  }
+                  else if (value instanceof SSLContext) {
+                     // ignore this one..
+                  }
+                  else {
+                     Map<String, String> inner = getProperties(value);
+                     for (Map.Entry<String, String> entry : inner.entrySet()) {
+                        properties.put(pd.getName() + "." + entry.getKey(), entry.getValue());
+                     }
+                  }
+               }
+            }
+         }
+      }
+
+      return properties;
+   }
+
+   /**
+    * Find a specific property getter in a given object based on a property name.
+    *
+    * @param object the object to search.
+    * @param name   the property name to search for.
+    * @return the result of invoking the specific property get method.
+    * @throws Exception if an error occurs while searching the object's bean info.
+    */
+   public static Object getProperty(Object object, String name) throws Exception {
+      BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
+      PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
+      if (propertyDescriptors != null) {
+         for (int i = 0; i < propertyDescriptors.length; i++) {
+            PropertyDescriptor pd = propertyDescriptors[i];
+            if (pd.getReadMethod() != null && pd.getName().equals(name)) {
+               return pd.getReadMethod().invoke(object);
+            }
+         }
+      }
+      return null;
+   }
+
+   /**
+    * Set a property named property on a given Object.
+    * <p>
+    * The object is searched for an set method that would match the given named
+    * property and if one is found.  If necessary an attempt will be made to convert
+    * the new value to an acceptable type.
+    *
+    * @param target The object whose property is to be set.
+    * @param name   The name of the property to set.
+    * @param value  The new value to set for the named property.
+    * @return true if the property was able to be set on the target object.
+    */
+   public static boolean setProperty(Object target, String name, Object value) {
+      try {
+         int dotPos = name.indexOf(".");
+         while (dotPos >= 0) {
+            String getterName = name.substring(0, dotPos);
+            target = getProperty(target, getterName);
+            name = name.substring(dotPos + 1);
+            dotPos = name.indexOf(".");
+         }
+
+         Class<?> clazz = target.getClass();
+         Method setter = findSetterMethod(clazz, name);
+         if (setter == null) {
+            return false;
+         }
+         // If the type is null or it matches the needed type, just use the
+         // value directly
+         if (value == null || value.getClass() == setter.getParameterTypes()[0]) {
+            setter.invoke(target, new Object[]{value});
+         }
+         else {
+            setter.invoke(target, new Object[]{convert(value, setter.getParameterTypes()[0])});
+         }
+         return true;
+      }
+      catch (Throwable ignore) {
+         return false;
+      }
+   }
+
+   /**
+    * Return a String minus the given prefix.  If the string does not start
+    * with the given prefix the original string value is returned.
+    *
+    * @param value  The String whose prefix is to be removed.
+    * @param prefix The prefix string to remove from the target string.
+    * @return stripped version of the original input string.
+    */
+   public static String stripPrefix(String value, String prefix) {
+      if (value != null && prefix != null && value.startsWith(prefix)) {
+         return value.substring(prefix.length());
+      }
+      return value;
+   }
+
+   /**
+    * Return a portion of a String value by looking beyond the given
+    * character.
+    *
+    * @param value The string value to split
+    * @param c     The character that marks the split point.
+    * @return the sub-string value starting beyond the given character.
+    */
+   public static String stripUpto(String value, char c) {
+      String result = null;
+      if (value != null) {
+         int index = value.indexOf(c);
+         if (index > 0) {
+            result = value.substring(index + 1);
+         }
+      }
+      return result;
+   }
+
+   /**
+    * Return a String up to and including character
+    *
+    * @param value The string value to split
+    * @param c     The character that marks the start of split point.
+    * @return the sub-string value starting from the given character.
+    */
+   public static String stripBefore(String value, char c) {
+      String result = value;
+      if (value != null) {
+         int index = value.indexOf(c);
+         if (index > 0) {
+            result = value.substring(0, index);
+         }
+      }
+      return result;
+   }
+
+   private static Method findSetterMethod(Class<?> clazz, String name) {
+      // Build the method name.
+      name = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
+      Method[] methods = clazz.getMethods();
+      for (int i = 0; i < methods.length; i++) {
+         Method method = methods[i];
+         Class<?>[] params = method.getParameterTypes();
+         if (method.getName().equals(name) && params.length == 1) {
+            return method;
+         }
+      }
+      return null;
+   }
+
+   private static Object convert(Object value, Class<?> type) throws Exception {
+      if (value == null) {
+         if (boolean.class.isAssignableFrom(type)) {
+            return Boolean.FALSE;
+         }
+         return null;
+      }
+
+      if (type.isAssignableFrom(value.getClass())) {
+         return type.cast(value);
+      }
+
+      // special for String[] as we do not want to use a PropertyEditor for that
+      if (type.isAssignableFrom(String[].class)) {
+         return StringArrayConverter.convertToStringArray(value);
+      }
+
+      if (type == URI.class) {
+         return new URI(value.toString());
+      }
+
+      return TypeConversionSupport.convert(value, type);
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/StringArrayConverter.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/StringArrayConverter.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/StringArrayConverter.java
new file mode 100644
index 0000000..3fc9eb4
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/StringArrayConverter.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.activemq.transport.amqp.client.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Class for converting to/from String[] to be used instead of a
+ * {@link java.beans.PropertyEditor} which otherwise causes memory leaks as the
+ * JDK {@link java.beans.PropertyEditorManager} is a static class and has strong
+ * references to classes, causing problems in hot-deployment environments.
+ */
+public class StringArrayConverter {
+
+   public static String[] convertToStringArray(Object value) {
+      if (value == null) {
+         return null;
+      }
+
+      String text = value.toString();
+      if (text == null || text.isEmpty()) {
+         return null;
+      }
+
+      StringTokenizer stok = new StringTokenizer(text, ",");
+      final List<String> list = new ArrayList<>();
+
+      while (stok.hasMoreTokens()) {
+         list.add(stok.nextToken());
+      }
+
+      String[] array = list.toArray(new String[list.size()]);
+      return array;
+   }
+
+   public static String convertToString(String[] value) {
+      if (value == null || value.length == 0) {
+         return null;
+      }
+
+      StringBuffer result = new StringBuffer(String.valueOf(value[0]));
+      for (int i = 1; i < value.length; i++) {
+         result.append(",").append(value[i]);
+      }
+
+      return result.toString();
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/TypeConversionSupport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/TypeConversionSupport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/TypeConversionSupport.java
new file mode 100644
index 0000000..7d07551
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/TypeConversionSupport.java
@@ -0,0 +1,218 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import java.util.Date;
+import java.util.HashMap;
+
+public final class TypeConversionSupport {
+
+   static class ConversionKey {
+
+      final Class<?> from;
+      final Class<?> to;
+      final int hashCode;
+
+      ConversionKey(Class<?> from, Class<?> to) {
+         this.from = from;
+         this.to = to;
+         this.hashCode = from.hashCode() ^ (to.hashCode() << 1);
+      }
+
+      @Override
+      public boolean equals(Object o) {
+         if (this == o) {
+            return true;
+         }
+
+         if (o == null || o.getClass() != this.getClass()) {
+            return false;
+         }
+
+         ConversionKey x = (ConversionKey) o;
+         return x.from == from && x.to == to;
+      }
+
+      @Override
+      public int hashCode() {
+         return hashCode;
+      }
+   }
+
+   interface Converter {
+
+      Object convert(Object value);
+   }
+
+   private static final HashMap<ConversionKey, Converter> CONVERSION_MAP = new HashMap<>();
+
+   static {
+      Converter toStringConverter = new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return value.toString();
+         }
+      };
+      CONVERSION_MAP.put(new ConversionKey(Boolean.class, String.class), toStringConverter);
+      CONVERSION_MAP.put(new ConversionKey(Byte.class, String.class), toStringConverter);
+      CONVERSION_MAP.put(new ConversionKey(Short.class, String.class), toStringConverter);
+      CONVERSION_MAP.put(new ConversionKey(Integer.class, String.class), toStringConverter);
+      CONVERSION_MAP.put(new ConversionKey(Long.class, String.class), toStringConverter);
+      CONVERSION_MAP.put(new ConversionKey(Float.class, String.class), toStringConverter);
+      CONVERSION_MAP.put(new ConversionKey(Double.class, String.class), toStringConverter);
+
+      CONVERSION_MAP.put(new ConversionKey(String.class, Boolean.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Boolean.valueOf((String) value);
+         }
+      });
+      CONVERSION_MAP.put(new ConversionKey(String.class, Byte.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Byte.valueOf((String) value);
+         }
+      });
+      CONVERSION_MAP.put(new ConversionKey(String.class, Short.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Short.valueOf((String) value);
+         }
+      });
+      CONVERSION_MAP.put(new ConversionKey(String.class, Integer.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Integer.valueOf((String) value);
+         }
+      });
+      CONVERSION_MAP.put(new ConversionKey(String.class, Long.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Long.valueOf((String) value);
+         }
+      });
+      CONVERSION_MAP.put(new ConversionKey(String.class, Float.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Float.valueOf((String) value);
+         }
+      });
+      CONVERSION_MAP.put(new ConversionKey(String.class, Double.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Double.valueOf((String) value);
+         }
+      });
+
+      Converter longConverter = new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Long.valueOf(((Number) value).longValue());
+         }
+      };
+      CONVERSION_MAP.put(new ConversionKey(Byte.class, Long.class), longConverter);
+      CONVERSION_MAP.put(new ConversionKey(Short.class, Long.class), longConverter);
+      CONVERSION_MAP.put(new ConversionKey(Integer.class, Long.class), longConverter);
+      CONVERSION_MAP.put(new ConversionKey(Date.class, Long.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Long.valueOf(((Date) value).getTime());
+         }
+      });
+
+      Converter intConverter = new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Integer.valueOf(((Number) value).intValue());
+         }
+      };
+      CONVERSION_MAP.put(new ConversionKey(Byte.class, Integer.class), intConverter);
+      CONVERSION_MAP.put(new ConversionKey(Short.class, Integer.class), intConverter);
+
+      CONVERSION_MAP.put(new ConversionKey(Byte.class, Short.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return Short.valueOf(((Number) value).shortValue());
+         }
+      });
+
+      CONVERSION_MAP.put(new ConversionKey(Float.class, Double.class), new Converter() {
+         @Override
+         public Object convert(Object value) {
+            return new Double(((Number) value).doubleValue());
+         }
+      });
+   }
+
+   public static Object convert(Object value, Class<?> toClass) {
+
+      assert value != null && toClass != null;
+
+      if (value.getClass() == toClass) {
+         return value;
+      }
+
+      Class<?> fromClass = value.getClass();
+
+      if (fromClass.isPrimitive()) {
+         fromClass = convertPrimitiveTypeToWrapperType(fromClass);
+      }
+
+      if (toClass.isPrimitive()) {
+         toClass = convertPrimitiveTypeToWrapperType(toClass);
+      }
+
+      Converter c = CONVERSION_MAP.get(new ConversionKey(fromClass, toClass));
+      if (c == null) {
+         return null;
+      }
+
+      return c.convert(value);
+   }
+
+   private static Class<?> convertPrimitiveTypeToWrapperType(Class<?> type) {
+      Class<?> rc = type;
+      if (type.isPrimitive()) {
+         if (type == int.class) {
+            rc = Integer.class;
+         }
+         else if (type == long.class) {
+            rc = Long.class;
+         }
+         else if (type == double.class) {
+            rc = Double.class;
+         }
+         else if (type == float.class) {
+            rc = Float.class;
+         }
+         else if (type == short.class) {
+            rc = Short.class;
+         }
+         else if (type == byte.class) {
+            rc = Byte.class;
+         }
+         else if (type == boolean.class) {
+            rc = Boolean.class;
+         }
+      }
+
+      return rc;
+   }
+
+   private TypeConversionSupport() {
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableConnection.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableConnection.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableConnection.java
new file mode 100644
index 0000000..32003a4
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableConnection.java
@@ -0,0 +1,202 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.transport.ErrorCondition;
+import org.apache.qpid.proton.engine.Collector;
+import org.apache.qpid.proton.engine.Connection;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.engine.EndpointState;
+import org.apache.qpid.proton.engine.Link;
+import org.apache.qpid.proton.engine.Record;
+import org.apache.qpid.proton.engine.Session;
+import org.apache.qpid.proton.engine.Transport;
+import org.apache.qpid.proton.reactor.Reactor;
+
+/**
+ * Unmodifiable Connection wrapper used to prevent test code from accidentally
+ * modifying Connection state.
+ */
+public class UnmodifiableConnection implements Connection {
+
+   private final Connection connection;
+
+   public UnmodifiableConnection(Connection connection) {
+      this.connection = connection;
+   }
+
+   @Override
+   public EndpointState getLocalState() {
+      return connection.getLocalState();
+   }
+
+   @Override
+   public EndpointState getRemoteState() {
+      return connection.getRemoteState();
+   }
+
+   @Override
+   public ErrorCondition getCondition() {
+      return connection.getCondition();
+   }
+
+   @Override
+   public void setCondition(ErrorCondition condition) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public ErrorCondition getRemoteCondition() {
+      return connection.getRemoteCondition();
+   }
+
+   @Override
+   public void free() {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public void open() {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public void close() {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public Session session() {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public Session sessionHead(EnumSet<EndpointState> local, EnumSet<EndpointState> remote) {
+      Session head = connection.sessionHead(local, remote);
+      if (head != null) {
+         head = new UnmodifiableSession(head);
+      }
+
+      return head;
+   }
+
+   @Override
+   public Link linkHead(EnumSet<EndpointState> local, EnumSet<EndpointState> remote) {
+      // TODO - If implemented this method should return an unmodifiable link isntance.
+      return null;
+   }
+
+   @Override
+   public Delivery getWorkHead() {
+      // TODO - If implemented this method should return an unmodifiable delivery isntance.
+      return null;
+   }
+
+   @Override
+   public void setContainer(String container) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public void setHostname(String hostname) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public String getHostname() {
+      return connection.getHostname();
+   }
+
+   @Override
+   public String getRemoteContainer() {
+      return connection.getRemoteContainer();
+   }
+
+   @Override
+   public String getRemoteHostname() {
+      return connection.getRemoteHostname();
+   }
+
+   @Override
+   public void setOfferedCapabilities(Symbol[] capabilities) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public void setDesiredCapabilities(Symbol[] capabilities) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public Symbol[] getRemoteOfferedCapabilities() {
+      return connection.getRemoteOfferedCapabilities();
+   }
+
+   @Override
+   public Symbol[] getRemoteDesiredCapabilities() {
+      return connection.getRemoteDesiredCapabilities();
+   }
+
+   @Override
+   public Map<Symbol, Object> getRemoteProperties() {
+      return connection.getRemoteProperties();
+   }
+
+   @Override
+   public void setProperties(Map<Symbol, Object> properties) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public Object getContext() {
+      return connection.getContext();
+   }
+
+   @Override
+   public void setContext(Object context) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public void collect(Collector collector) {
+      throw new UnsupportedOperationException("Cannot alter the Connection");
+   }
+
+   @Override
+   public String getContainer() {
+      return connection.getContainer();
+   }
+
+   @Override
+   public Transport getTransport() {
+      return new UnmodifiableTransport(connection.getTransport());
+   }
+
+   @Override
+   public Record attachments() {
+      return connection.attachments();
+   }
+
+   @Override
+   public Reactor getReactor() {
+      return connection.getReactor();
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableDelivery.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableDelivery.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableDelivery.java
new file mode 100644
index 0000000..9f48b41
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableDelivery.java
@@ -0,0 +1,170 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import org.apache.qpid.proton.amqp.transport.DeliveryState;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.engine.Link;
+import org.apache.qpid.proton.engine.Receiver;
+import org.apache.qpid.proton.engine.Record;
+import org.apache.qpid.proton.engine.Sender;
+
+/**
+ * Unmodifiable Delivery wrapper used to prevent test code from accidentally
+ * modifying Delivery state.
+ */
+public class UnmodifiableDelivery implements Delivery {
+
+   private final Delivery delivery;
+
+   public UnmodifiableDelivery(Delivery delivery) {
+      this.delivery = delivery;
+   }
+
+   @Override
+   public byte[] getTag() {
+      return delivery.getTag();
+   }
+
+   @Override
+   public Link getLink() {
+      if (delivery.getLink() instanceof Sender) {
+         return new UnmodifiableSender((Sender) delivery.getLink());
+      }
+      else if (delivery.getLink() instanceof Receiver) {
+         return new UnmodifiableReceiver((Receiver) delivery.getLink());
+      }
+      else {
+         throw new IllegalStateException("Delivery has unknown link type");
+      }
+   }
+
+   @Override
+   public DeliveryState getLocalState() {
+      return delivery.getLocalState();
+   }
+
+   @Override
+   public DeliveryState getRemoteState() {
+      return delivery.getRemoteState();
+   }
+
+   @Override
+   public int getMessageFormat() {
+      return delivery.getMessageFormat();
+   }
+
+   @Override
+   public void disposition(DeliveryState state) {
+      throw new UnsupportedOperationException("Cannot alter the Delivery state");
+   }
+
+   @Override
+   public void settle() {
+      throw new UnsupportedOperationException("Cannot alter the Delivery state");
+   }
+
+   @Override
+   public boolean isSettled() {
+      return delivery.isSettled();
+   }
+
+   @Override
+   public boolean remotelySettled() {
+      return delivery.remotelySettled();
+   }
+
+   @Override
+   public void free() {
+      throw new UnsupportedOperationException("Cannot alter the Delivery state");
+   }
+
+   @Override
+   public Delivery getWorkNext() {
+      return new UnmodifiableDelivery(delivery.getWorkNext());
+   }
+
+   @Override
+   public Delivery next() {
+      return new UnmodifiableDelivery(delivery.next());
+   }
+
+   @Override
+   public boolean isWritable() {
+      return delivery.isWritable();
+   }
+
+   @Override
+   public boolean isReadable() {
+      return delivery.isReadable();
+   }
+
+   @Override
+   public void setContext(Object o) {
+      throw new UnsupportedOperationException("Cannot alter the Delivery state");
+   }
+
+   @Override
+   public Object getContext() {
+      return delivery.getContext();
+   }
+
+   @Override
+   public boolean isUpdated() {
+      return delivery.isUpdated();
+   }
+
+   @Override
+   public void clear() {
+      throw new UnsupportedOperationException("Cannot alter the Delivery state");
+   }
+
+   @Override
+   public boolean isPartial() {
+      return delivery.isPartial();
+   }
+
+   @Override
+   public int pending() {
+      return delivery.pending();
+   }
+
+   @Override
+   public boolean isBuffered() {
+      return delivery.isBuffered();
+   }
+
+   @Override
+   public Record attachments() {
+      return delivery.attachments();
+   }
+
+   @Override
+   public DeliveryState getDefaultDeliveryState() {
+      return delivery.getDefaultDeliveryState();
+   }
+
+   @Override
+   public void setDefaultDeliveryState(DeliveryState state) {
+      throw new UnsupportedOperationException("Cannot alter the Delivery");
+   }
+
+   @Override
+   public void setMessageFormat(int messageFormat) {
+      throw new UnsupportedOperationException("Cannot alter the Delivery");
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableLink.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableLink.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableLink.java
new file mode 100644
index 0000000..a58bfe7
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableLink.java
@@ -0,0 +1,276 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.transport.ErrorCondition;
+import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
+import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
+import org.apache.qpid.proton.amqp.transport.Source;
+import org.apache.qpid.proton.amqp.transport.Target;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.engine.EndpointState;
+import org.apache.qpid.proton.engine.Link;
+import org.apache.qpid.proton.engine.Receiver;
+import org.apache.qpid.proton.engine.Record;
+import org.apache.qpid.proton.engine.Sender;
+import org.apache.qpid.proton.engine.Session;
+
+/**
+ * Unmodifiable Session wrapper used to prevent test code from accidentally
+ * modifying Session state.
+ */
+public class UnmodifiableLink implements Link {
+
+   private final Link link;
+
+   public UnmodifiableLink(Link link) {
+      this.link = link;
+   }
+
+   @Override
+   public EndpointState getLocalState() {
+      return link.getLocalState();
+   }
+
+   @Override
+   public EndpointState getRemoteState() {
+      return link.getRemoteState();
+   }
+
+   @Override
+   public ErrorCondition getCondition() {
+      return link.getCondition();
+   }
+
+   @Override
+   public void setCondition(ErrorCondition condition) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public ErrorCondition getRemoteCondition() {
+      return link.getRemoteCondition();
+   }
+
+   @Override
+   public void free() {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public void open() {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public void close() {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public void setContext(Object o) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public Object getContext() {
+      return link.getContext();
+   }
+
+   @Override
+   public String getName() {
+      return link.getName();
+   }
+
+   @Override
+   public Delivery delivery(byte[] tag) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public Delivery delivery(byte[] tag, int offset, int length) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public Delivery head() {
+      return new UnmodifiableDelivery(link.head());
+   }
+
+   @Override
+   public Delivery current() {
+      return new UnmodifiableDelivery(link.current());
+   }
+
+   @Override
+   public boolean advance() {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public Source getSource() {
+      // TODO Figure out a simple way to wrap the odd Source types in Proton-J
+      return link.getSource();
+   }
+
+   @Override
+   public Target getTarget() {
+      // TODO Figure out a simple way to wrap the odd Source types in Proton-J
+      return link.getTarget();
+   }
+
+   @Override
+   public void setSource(Source address) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public void setTarget(Target address) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public Source getRemoteSource() {
+      // TODO Figure out a simple way to wrap the odd Source types in Proton-J
+      return link.getRemoteSource();
+   }
+
+   @Override
+   public Target getRemoteTarget() {
+      // TODO Figure out a simple way to wrap the odd Target types in Proton-J
+      return link.getRemoteTarget();
+   }
+
+   @Override
+   public Link next(EnumSet<EndpointState> local, EnumSet<EndpointState> remote) {
+      Link next = link.next(local, remote);
+
+      if (next != null) {
+         if (next instanceof Sender) {
+            next = new UnmodifiableSender((Sender) next);
+         }
+         else {
+            next = new UnmodifiableReceiver((Receiver) next);
+         }
+      }
+
+      return next;
+   }
+
+   @Override
+   public int getCredit() {
+      return link.getCredit();
+   }
+
+   @Override
+   public int getQueued() {
+      return link.getQueued();
+   }
+
+   @Override
+   public int getUnsettled() {
+      return link.getUnsettled();
+   }
+
+   @Override
+   public Session getSession() {
+      return new UnmodifiableSession(link.getSession());
+   }
+
+   @Override
+   public SenderSettleMode getSenderSettleMode() {
+      return link.getSenderSettleMode();
+   }
+
+   @Override
+   public void setSenderSettleMode(SenderSettleMode senderSettleMode) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public SenderSettleMode getRemoteSenderSettleMode() {
+      return link.getRemoteSenderSettleMode();
+   }
+
+   @Override
+   public ReceiverSettleMode getReceiverSettleMode() {
+      return link.getReceiverSettleMode();
+   }
+
+   @Override
+   public void setReceiverSettleMode(ReceiverSettleMode receiverSettleMode) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public ReceiverSettleMode getRemoteReceiverSettleMode() {
+      return link.getRemoteReceiverSettleMode();
+   }
+
+   @Override
+   public void setRemoteSenderSettleMode(SenderSettleMode remoteSenderSettleMode) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public int drained() {
+      return link.drained();  // TODO - Is this a mutating call?
+   }
+
+   @Override
+   public int getRemoteCredit() {
+      return link.getRemoteCredit();
+   }
+
+   @Override
+   public boolean getDrain() {
+      return link.getDrain();
+   }
+
+   @Override
+   public void detach() {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public boolean detached() {
+      return link.detached();
+   }
+
+   public Record attachments() {
+      return link.attachments();
+   }
+
+   @Override
+   public Map<Symbol, Object> getProperties() {
+      return link.getProperties();
+   }
+
+   @Override
+   public void setProperties(Map<Symbol, Object> properties) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public Map<Symbol, Object> getRemoteProperties() {
+      return link.getRemoteProperties();
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableReceiver.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableReceiver.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableReceiver.java
new file mode 100644
index 0000000..92760db
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableReceiver.java
@@ -0,0 +1,59 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import org.apache.qpid.proton.engine.Receiver;
+
+/**
+ * Unmodifiable Receiver wrapper used to prevent test code from accidentally
+ * modifying Receiver state.
+ */
+public class UnmodifiableReceiver extends UnmodifiableLink implements Receiver {
+
+   private final Receiver receiver;
+
+   public UnmodifiableReceiver(Receiver receiver) {
+      super(receiver);
+
+      this.receiver = receiver;
+   }
+
+   @Override
+   public void flow(int credits) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public int recv(byte[] bytes, int offset, int size) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public void drain(int credit) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public boolean draining() {
+      return receiver.draining();
+   }
+
+   @Override
+   public void setDrain(boolean drain) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSender.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSender.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSender.java
new file mode 100644
index 0000000..89742cb
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSender.java
@@ -0,0 +1,45 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import org.apache.qpid.proton.engine.Sender;
+
+/**
+ * Unmodifiable Sender wrapper used to prevent test code from accidentally
+ * modifying Sender state.
+ */
+public class UnmodifiableSender extends UnmodifiableLink implements Sender {
+
+   public UnmodifiableSender(Sender sender) {
+      super(sender);
+   }
+
+   @Override
+   public void offer(int credits) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public int send(byte[] bytes, int offset, int length) {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+
+   @Override
+   public void abort() {
+      throw new UnsupportedOperationException("Cannot alter the Link state");
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSession.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSession.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSession.java
new file mode 100644
index 0000000..a44028e
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableSession.java
@@ -0,0 +1,150 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import java.util.EnumSet;
+
+import org.apache.qpid.proton.amqp.transport.ErrorCondition;
+import org.apache.qpid.proton.engine.Connection;
+import org.apache.qpid.proton.engine.EndpointState;
+import org.apache.qpid.proton.engine.Receiver;
+import org.apache.qpid.proton.engine.Record;
+import org.apache.qpid.proton.engine.Sender;
+import org.apache.qpid.proton.engine.Session;
+
+/**
+ * Unmodifiable Session wrapper used to prevent test code from accidentally
+ * modifying Session state.
+ */
+public class UnmodifiableSession implements Session {
+
+   private final Session session;
+
+   public UnmodifiableSession(Session session) {
+      this.session = session;
+   }
+
+   @Override
+   public EndpointState getLocalState() {
+      return session.getLocalState();
+   }
+
+   @Override
+   public EndpointState getRemoteState() {
+      return session.getRemoteState();
+   }
+
+   @Override
+   public ErrorCondition getCondition() {
+      return session.getCondition();
+   }
+
+   @Override
+   public void setCondition(ErrorCondition condition) {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public ErrorCondition getRemoteCondition() {
+      return session.getRemoteCondition();
+   }
+
+   @Override
+   public void free() {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public void open() {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public void close() {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public void setContext(Object o) {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public Object getContext() {
+      return session.getContext();
+   }
+
+   @Override
+   public Sender sender(String name) {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public Receiver receiver(String name) {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public Session next(EnumSet<EndpointState> local, EnumSet<EndpointState> remote) {
+      Session next = session.next(local, remote);
+      if (next != null) {
+         next = new UnmodifiableSession(next);
+      }
+
+      return next;
+   }
+
+   @Override
+   public Connection getConnection() {
+      return new UnmodifiableConnection(session.getConnection());
+   }
+
+   @Override
+   public int getIncomingCapacity() {
+      return session.getIncomingCapacity();
+   }
+
+   @Override
+   public void setIncomingCapacity(int bytes) {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+
+   @Override
+   public int getIncomingBytes() {
+      return session.getIncomingBytes();
+   }
+
+   @Override
+   public int getOutgoingBytes() {
+      return session.getOutgoingBytes();
+   }
+
+   @Override
+   public Record attachments() {
+      return session.attachments();
+   }
+
+   @Override
+   public long getOutgoingWindow() {
+      return session.getOutgoingWindow();
+   }
+
+   @Override
+   public void setOutgoingWindow(long outgoingWindowSize) {
+      throw new UnsupportedOperationException("Cannot alter the Session");
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableTransport.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableTransport.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableTransport.java
new file mode 100644
index 0000000..5e305f4
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/UnmodifiableTransport.java
@@ -0,0 +1,274 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+import java.nio.ByteBuffer;
+
+import org.apache.qpid.proton.amqp.transport.ErrorCondition;
+import org.apache.qpid.proton.engine.Connection;
+import org.apache.qpid.proton.engine.EndpointState;
+import org.apache.qpid.proton.engine.Record;
+import org.apache.qpid.proton.engine.Sasl;
+import org.apache.qpid.proton.engine.Ssl;
+import org.apache.qpid.proton.engine.SslDomain;
+import org.apache.qpid.proton.engine.SslPeerDetails;
+import org.apache.qpid.proton.engine.Transport;
+import org.apache.qpid.proton.engine.TransportException;
+import org.apache.qpid.proton.engine.TransportResult;
+
+/**
+ * Unmodifiable Transport wrapper used to prevent test code from accidentally
+ * modifying Transport state.
+ */
+public class UnmodifiableTransport implements Transport {
+
+   private final Transport transport;
+
+   public UnmodifiableTransport(Transport transport) {
+      this.transport = transport;
+   }
+
+   @Override
+   public void close() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void free() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public Object getContext() {
+      return null;
+   }
+
+   @Override
+   public EndpointState getLocalState() {
+      return transport.getLocalState();
+   }
+
+   @Override
+   public ErrorCondition getRemoteCondition() {
+      return transport.getRemoteCondition();
+   }
+
+   @Override
+   public EndpointState getRemoteState() {
+      return transport.getRemoteState();
+   }
+
+   @Override
+   public void open() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void setCondition(ErrorCondition arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void setContext(Object arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void bind(Connection arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public int capacity() {
+      return transport.capacity();
+   }
+
+   @Override
+   public void close_head() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void close_tail() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public int getChannelMax() {
+      return transport.getChannelMax();
+   }
+
+   @Override
+   public ErrorCondition getCondition() {
+      return transport.getCondition();
+   }
+
+   @Override
+   public int getIdleTimeout() {
+      return transport.getIdleTimeout();
+   }
+
+   @Override
+   public ByteBuffer getInputBuffer() {
+      return null;
+   }
+
+   @Override
+   public int getMaxFrameSize() {
+      return transport.getMaxFrameSize();
+   }
+
+   @Override
+   public ByteBuffer getOutputBuffer() {
+      return null;
+   }
+
+   @Override
+   public int getRemoteChannelMax() {
+      return transport.getRemoteChannelMax();
+   }
+
+   @Override
+   public int getRemoteIdleTimeout() {
+      return transport.getRemoteIdleTimeout();
+   }
+
+   @Override
+   public int getRemoteMaxFrameSize() {
+      return transport.getRemoteMaxFrameSize();
+   }
+
+   @Override
+   public ByteBuffer head() {
+      return null;
+   }
+
+   @Override
+   public int input(byte[] arg0, int arg1, int arg2) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public boolean isClosed() {
+      return transport.isClosed();
+   }
+
+   @Override
+   public int output(byte[] arg0, int arg1, int arg2) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void outputConsumed() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public int pending() {
+      return transport.pending();
+   }
+
+   @Override
+   public void pop(int arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void process() throws TransportException {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public TransportResult processInput() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public Sasl sasl() throws IllegalStateException {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void setChannelMax(int arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void setIdleTimeout(int arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void setMaxFrameSize(int arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public Ssl ssl(SslDomain arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public Ssl ssl(SslDomain arg0, SslPeerDetails arg1) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public ByteBuffer tail() {
+      return null;
+   }
+
+   @Override
+   public long tick(long arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void trace(int arg0) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public void unbind() {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public Record attachments() {
+      return transport.attachments();
+   }
+
+   @Override
+   public long getFramesInput() {
+      return transport.getFramesInput();
+   }
+
+   @Override
+   public long getFramesOutput() {
+      return transport.getFramesOutput();
+   }
+
+   @Override
+   public void setEmitFlowEventOnSend(boolean emitFlowEventOnSend) {
+      throw new UnsupportedOperationException("Cannot alter the Transport");
+   }
+
+   @Override
+   public boolean isEmitFlowEventOnSend() {
+      return transport.isEmitFlowEventOnSend();
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/WrappedAsyncResult.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/WrappedAsyncResult.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/WrappedAsyncResult.java
new file mode 100644
index 0000000..bfe9a80
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/util/WrappedAsyncResult.java
@@ -0,0 +1,59 @@
+/**
+ * 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.activemq.transport.amqp.client.util;
+
+/**
+ * Base class used to wrap one AsyncResult with another.
+ */
+public abstract class WrappedAsyncResult implements AsyncResult {
+
+   protected final AsyncResult wrapped;
+
+   /**
+    * Create a new WrappedAsyncResult for the target AsyncResult
+    */
+   public WrappedAsyncResult(AsyncResult wrapped) {
+      this.wrapped = wrapped;
+   }
+
+   @Override
+   public void onFailure(Throwable result) {
+      if (wrapped != null) {
+         wrapped.onFailure(result);
+      }
+   }
+
+   @Override
+   public void onSuccess() {
+      if (wrapped != null) {
+         wrapped.onSuccess();
+      }
+   }
+
+   @Override
+   public boolean isComplete() {
+      if (wrapped != null) {
+         return wrapped.isComplete();
+      }
+
+      return false;
+   }
+
+   public AsyncResult getWrappedRequest() {
+      return wrapped;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/integration-tests/pom.xml
----------------------------------------------------------------------
diff --git a/tests/integration-tests/pom.xml b/tests/integration-tests/pom.xml
index 752e288..5d7617c 100644
--- a/tests/integration-tests/pom.xml
+++ b/tests/integration-tests/pom.xml
@@ -340,6 +340,11 @@
 		   <artifactId>org.apache.karaf.shell.console</artifactId>
 		   <version>${karaf.version}</version>
 	   </dependency>
+      <dependency>
+         <groupId>org.apache.activemq.tests</groupId>
+         <artifactId>artemis-test-support</artifactId>
+         <version>${project.version}</version>
+      </dependency>
    </dependencies>
 
    <build>

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/pom.xml
----------------------------------------------------------------------
diff --git a/tests/pom.xml b/tests/pom.xml
index 6a9c000..a2efeac 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -46,6 +46,13 @@
             <version>1.2</version>
             <!-- License: Apache: 2.0 -->
          </dependency>
+         <dependency>
+            <groupId>org.apache.qpid</groupId>
+            <artifactId>qpid-jms-client</artifactId>
+            <version>0.10.0</version>
+            <!-- License: Apache: 2.0 -->
+         </dependency>
+
          <!-- End JMS Dependencies -->
       </dependencies>
    </dependencyManagement>
@@ -122,5 +129,6 @@
       <module>soak-tests</module>
       <module>stress-tests</module>
       <module>performance-tests</module>
+      <module>artemis-test-support</module>
    </modules>
 </project>


[8/9] activemq-artemis git commit: ARTEMIS-637 Port 5.x AMQP test client

Posted by an...@apache.org.
http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpMessage.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpMessage.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpMessage.java
new file mode 100644
index 0000000..320d174
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpMessage.java
@@ -0,0 +1,515 @@
+/*
+ * 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.activemq.transport.amqp.client;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.activemq.transport.amqp.client.util.UnmodifiableDelivery;
+import org.apache.qpid.proton.Proton;
+import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.DescribedType;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.AmqpValue;
+import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
+import org.apache.qpid.proton.amqp.messaging.Data;
+import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
+import org.apache.qpid.proton.amqp.messaging.Header;
+import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
+import org.apache.qpid.proton.amqp.messaging.Properties;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.message.Message;
+
+public class AmqpMessage {
+
+   private final AmqpReceiver receiver;
+   private final Message message;
+   private final Delivery delivery;
+
+   private Map<Symbol, Object> deliveryAnnotationsMap;
+   private Map<Symbol, Object> messageAnnotationsMap;
+   private Map<String, Object> applicationPropertiesMap;
+
+   /**
+    * Creates a new AmqpMessage that wraps the information necessary to handle
+    * an outgoing message.
+    */
+   public AmqpMessage() {
+      receiver = null;
+      delivery = null;
+
+      message = Proton.message();
+   }
+
+   /**
+    * Creates a new AmqpMessage that wraps the information necessary to handle
+    * an outgoing message.
+    *
+    * @param message the Proton message that is to be sent.
+    */
+   public AmqpMessage(Message message) {
+      this(null, message, null);
+   }
+
+   /**
+    * Creates a new AmqpMessage that wraps the information necessary to handle
+    * an incoming delivery.
+    *
+    * @param receiver the AmqpReceiver that received this message.
+    * @param message  the Proton message that was received.
+    * @param delivery the Delivery instance that produced this message.
+    */
+   @SuppressWarnings("unchecked")
+   public AmqpMessage(AmqpReceiver receiver, Message message, Delivery delivery) {
+      this.receiver = receiver;
+      this.message = message;
+      this.delivery = delivery;
+
+      if (message.getMessageAnnotations() != null) {
+         messageAnnotationsMap = message.getMessageAnnotations().getValue();
+      }
+
+      if (message.getApplicationProperties() != null) {
+         applicationPropertiesMap = message.getApplicationProperties().getValue();
+      }
+
+      if (message.getDeliveryAnnotations() != null) {
+         deliveryAnnotationsMap = message.getDeliveryAnnotations().getValue();
+      }
+   }
+
+   //----- Access to interal client resources -------------------------------//
+
+   /**
+    * @return the AMQP Delivery object linked to a received message.
+    */
+   public Delivery getWrappedDelivery() {
+      if (delivery != null) {
+         return new UnmodifiableDelivery(delivery);
+      }
+
+      return null;
+   }
+
+   /**
+    * @return the AMQP Message that is wrapped by this object.
+    */
+   public Message getWrappedMessage() {
+      return message;
+   }
+
+   /**
+    * @return the AmqpReceiver that consumed this message.
+    */
+   public AmqpReceiver getAmqpReceiver() {
+      return receiver;
+   }
+
+   //----- Message disposition control --------------------------------------//
+
+   /**
+    * Accepts the message marking it as consumed on the remote peer.
+    *
+    * @throws Exception if an error occurs during the accept.
+    */
+   public void accept() throws Exception {
+      if (receiver == null) {
+         throw new IllegalStateException("Can't accept non-received message.");
+      }
+
+      receiver.accept(delivery);
+   }
+
+   /**
+    * Marks the message as Modified, indicating whether it failed to deliver and is not deliverable here.
+    *
+    * @param deliveryFailed    indicates that the delivery failed for some reason.
+    * @param undeliverableHere marks the delivery as not being able to be process by link it was sent to.
+    * @throws Exception if an error occurs during the process.
+    */
+   public void modified(Boolean deliveryFailed, Boolean undeliverableHere) throws Exception {
+      if (receiver == null) {
+         throw new IllegalStateException("Can't modify non-received message.");
+      }
+
+      receiver.modified(delivery, deliveryFailed, undeliverableHere);
+   }
+
+   /**
+    * Release the message, remote can redeliver it elsewhere.
+    *
+    * @throws Exception if an error occurs during the reject.
+    */
+   public void release() throws Exception {
+      if (receiver == null) {
+         throw new IllegalStateException("Can't release non-received message.");
+      }
+
+      receiver.release(delivery);
+   }
+
+   //----- Convenience methods for constructing outbound messages -----------//
+
+   /**
+    * Sets the MessageId property on an outbound message using the provided String
+    *
+    * @param messageId the String message ID value to set.
+    */
+   public void setMessageId(String messageId) {
+      checkReadOnly();
+      lazyCreateProperties();
+      getWrappedMessage().setMessageId(messageId);
+   }
+
+   /**
+    * Return the set MessageId value in String form, if there are no properties
+    * in the given message return null.
+    *
+    * @return the set message ID in String form or null if not set.
+    */
+   public String getMessageId() {
+      if (message.getProperties() == null) {
+         return null;
+      }
+
+      return message.getProperties().getMessageId().toString();
+   }
+
+   /**
+    * Return the set MessageId value in the original form, if there are no properties
+    * in the given message return null.
+    *
+    * @return the set message ID in its original form or null if not set.
+    */
+   public Object getRawMessageId() {
+      if (message.getProperties() == null) {
+         return null;
+      }
+
+      return message.getProperties().getMessageId();
+   }
+
+   /**
+    * Sets the MessageId property on an outbound message using the provided value
+    *
+    * @param messageId the message ID value to set.
+    */
+   public void setRawMessageId(Object messageId) {
+      checkReadOnly();
+      lazyCreateProperties();
+      getWrappedMessage().setMessageId(messageId);
+   }
+
+   /**
+    * Sets the CorrelationId property on an outbound message using the provided String
+    *
+    * @param correlationId the String Correlation ID value to set.
+    */
+   public void setCorrelationId(String correlationId) {
+      checkReadOnly();
+      lazyCreateProperties();
+      getWrappedMessage().setCorrelationId(correlationId);
+   }
+
+   /**
+    * Return the set CorrelationId value in String form, if there are no properties
+    * in the given message return null.
+    *
+    * @return the set correlation ID in String form or null if not set.
+    */
+   public String getCorrelationId() {
+      if (message.getProperties() == null) {
+         return null;
+      }
+
+      return message.getProperties().getCorrelationId().toString();
+   }
+
+   /**
+    * Return the set CorrelationId value in the original form, if there are no properties
+    * in the given message return null.
+    *
+    * @return the set message ID in its original form or null if not set.
+    */
+   public Object getRawCorrelationId() {
+      if (message.getProperties() == null) {
+         return null;
+      }
+
+      return message.getProperties().getCorrelationId();
+   }
+
+   /**
+    * Sets the CorrelationId property on an outbound message using the provided value
+    *
+    * @param correlationId the correlation ID value to set.
+    */
+   public void setRawCorrelationId(Object correlationId) {
+      checkReadOnly();
+      lazyCreateProperties();
+      getWrappedMessage().setCorrelationId(correlationId);
+   }
+
+   /**
+    * Sets the GroupId property on an outbound message using the provided String
+    *
+    * @param groupId the String Group ID value to set.
+    */
+   public void setGroupId(String groupId) {
+      checkReadOnly();
+      lazyCreateProperties();
+      getWrappedMessage().setGroupId(groupId);
+   }
+
+   /**
+    * Return the set GroupId value in String form, if there are no properties
+    * in the given message return null.
+    *
+    * @return the set GroupID in String form or null if not set.
+    */
+   public String getGroupId() {
+      if (message.getProperties() == null) {
+         return null;
+      }
+
+      return message.getProperties().getGroupId();
+   }
+
+   /**
+    * Sets the durable header on the outgoing message.
+    *
+    * @param durable the boolean durable value to set.
+    */
+   public void setDurable(boolean durable) {
+      checkReadOnly();
+      lazyCreateHeader();
+      getWrappedMessage().setDurable(durable);
+   }
+
+   /**
+    * Checks the durable value in the Message Headers to determine if
+    * the message was sent as a durable Message.
+    *
+    * @return true if the message is marked as being durable.
+    */
+   public boolean isDurable() {
+      if (message.getHeader() == null) {
+         return false;
+      }
+
+      return message.getHeader().getDurable();
+   }
+
+   /**
+    * Sets a given application property on an outbound message.
+    *
+    * @param key   the name to assign the new property.
+    * @param value the value to set for the named property.
+    */
+   public void setApplicationProperty(String key, Object value) {
+      checkReadOnly();
+      lazyCreateApplicationProperties();
+      applicationPropertiesMap.put(key, value);
+   }
+
+   /**
+    * Gets the application property that is mapped to the given name or null
+    * if no property has been set with that name.
+    *
+    * @param key the name used to lookup the property in the application properties.
+    * @return the propety value or null if not set.
+    */
+   public Object getApplicationProperty(String key) {
+      if (applicationPropertiesMap == null) {
+         return null;
+      }
+
+      return applicationPropertiesMap.get(key);
+   }
+
+   /**
+    * Perform a proper annotation set on the AMQP Message based on a Symbol key and
+    * the target value to append to the current annotations.
+    *
+    * @param key   The name of the Symbol whose value is being set.
+    * @param value The new value to set in the annotations of this message.
+    */
+   public void setMessageAnnotation(String key, Object value) {
+      checkReadOnly();
+      lazyCreateMessageAnnotations();
+      messageAnnotationsMap.put(Symbol.valueOf(key), value);
+   }
+
+   /**
+    * Given a message annotation name, lookup and return the value associated with
+    * that annotation name.  If the message annotations have not been created yet
+    * then this method will always return null.
+    *
+    * @param key the Symbol name that should be looked up in the message annotations.
+    * @return the value of the annotation if it exists, or null if not set or not accessible.
+    */
+   public Object getMessageAnnotation(String key) {
+      if (messageAnnotationsMap == null) {
+         return null;
+      }
+
+      return messageAnnotationsMap.get(Symbol.valueOf(key));
+   }
+
+   /**
+    * Perform a proper delivery annotation set on the AMQP Message based on a Symbol
+    * key and the target value to append to the current delivery annotations.
+    *
+    * @param key   The name of the Symbol whose value is being set.
+    * @param value The new value to set in the delivery annotations of this message.
+    */
+   public void setDeliveryAnnotation(String key, Object value) {
+      checkReadOnly();
+      lazyCreateDeliveryAnnotations();
+      deliveryAnnotationsMap.put(Symbol.valueOf(key), value);
+   }
+
+   /**
+    * Given a message annotation name, lookup and return the value associated with
+    * that annotation name.  If the message annotations have not been created yet
+    * then this method will always return null.
+    *
+    * @param key the Symbol name that should be looked up in the message annotations.
+    * @return the value of the annotation if it exists, or null if not set or not accessible.
+    */
+   public Object getDeliveryAnnotation(String key) {
+      if (deliveryAnnotationsMap == null) {
+         return null;
+      }
+
+      return deliveryAnnotationsMap.get(Symbol.valueOf(key));
+   }
+
+   //----- Methods for manipulating the Message body ------------------------//
+
+   /**
+    * Sets a String value into the body of an outgoing Message, throws
+    * an exception if this is an incoming message instance.
+    *
+    * @param value the String value to store in the Message body.
+    * @throws IllegalStateException if the message is read only.
+    */
+   public void setText(String value) throws IllegalStateException {
+      checkReadOnly();
+      AmqpValue body = new AmqpValue(value);
+      getWrappedMessage().setBody(body);
+   }
+
+   /**
+    * Sets a byte array value into the body of an outgoing Message, throws
+    * an exception if this is an incoming message instance.
+    *
+    * @param bytes the byte array value to store in the Message body.
+    * @throws IllegalStateException if the message is read only.
+    */
+   public void setBytes(byte[] bytes) throws IllegalStateException {
+      checkReadOnly();
+      Data body = new Data(new Binary(bytes));
+      getWrappedMessage().setBody(body);
+   }
+
+   /**
+    * Sets a byte array value into the body of an outgoing Message, throws
+    * an exception if this is an incoming message instance.
+    *
+    * @param described the byte array value to store in the Message body.
+    * @throws IllegalStateException if the message is read only.
+    */
+   public void setDescribedType(DescribedType described) throws IllegalStateException {
+      checkReadOnly();
+      AmqpValue body = new AmqpValue(described);
+      getWrappedMessage().setBody(body);
+   }
+
+   /**
+    * Attempts to retrieve the message body as an DescribedType instance.
+    *
+    * @return an DescribedType instance if one is stored in the message body.
+    * @throws NoSuchElementException if the body does not contain a DescribedType.
+    */
+   public DescribedType getDescribedType() throws NoSuchElementException {
+      DescribedType result = null;
+
+      if (getWrappedMessage().getBody() == null) {
+         return null;
+      }
+      else {
+         if (getWrappedMessage().getBody() instanceof AmqpValue) {
+            AmqpValue value = (AmqpValue) getWrappedMessage().getBody();
+
+            if (value.getValue() == null) {
+               result = null;
+            }
+            else if (value.getValue() instanceof DescribedType) {
+               result = (DescribedType) value.getValue();
+            }
+            else {
+               throw new NoSuchElementException("Message does not contain a DescribedType body");
+            }
+         }
+      }
+
+      return result;
+   }
+
+   //----- Internal implementation ------------------------------------------//
+
+   private void checkReadOnly() throws IllegalStateException {
+      if (delivery != null) {
+         throw new IllegalStateException("Message is read only.");
+      }
+   }
+
+   private void lazyCreateMessageAnnotations() {
+      if (messageAnnotationsMap == null) {
+         messageAnnotationsMap = new HashMap<>();
+         message.setMessageAnnotations(new MessageAnnotations(messageAnnotationsMap));
+      }
+   }
+
+   private void lazyCreateDeliveryAnnotations() {
+      if (deliveryAnnotationsMap == null) {
+         deliveryAnnotationsMap = new HashMap<>();
+         message.setDeliveryAnnotations(new DeliveryAnnotations(deliveryAnnotationsMap));
+      }
+   }
+
+   private void lazyCreateApplicationProperties() {
+      if (applicationPropertiesMap == null) {
+         applicationPropertiesMap = new HashMap<>();
+         message.setApplicationProperties(new ApplicationProperties(applicationPropertiesMap));
+      }
+   }
+
+   private void lazyCreateHeader() {
+      if (message.getHeader() == null) {
+         message.setHeader(new Header());
+      }
+   }
+
+   private void lazyCreateProperties() {
+      if (message.getProperties() == null) {
+         message.setProperties(new Properties());
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpNoLocalFilter.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpNoLocalFilter.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpNoLocalFilter.java
new file mode 100644
index 0000000..2e36e84
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpNoLocalFilter.java
@@ -0,0 +1,45 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import org.apache.qpid.proton.amqp.DescribedType;
+
+import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_CODE;
+
+/**
+ * A Described Type wrapper for JMS no local option for MessageConsumer.
+ */
+public class AmqpNoLocalFilter implements DescribedType {
+
+   public static final AmqpNoLocalFilter NO_LOCAL = new AmqpNoLocalFilter();
+
+   private final String noLocal;
+
+   public AmqpNoLocalFilter() {
+      this.noLocal = "NoLocalFilter{}";
+   }
+
+   @Override
+   public Object getDescriptor() {
+      return NO_LOCAL_CODE;
+   }
+
+   @Override
+   public Object getDescribed() {
+      return this.noLocal;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpReceiver.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpReceiver.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpReceiver.java
new file mode 100644
index 0000000..9f3bff2
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpReceiver.java
@@ -0,0 +1,946 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import javax.jms.InvalidDestinationException;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.activemq.transport.amqp.client.util.ClientFuture;
+import org.apache.activemq.transport.amqp.client.util.IOExceptionSupport;
+import org.apache.activemq.transport.amqp.client.util.UnmodifiableReceiver;
+import org.apache.qpid.jms.JmsOperationTimedOutException;
+import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.DescribedType;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.Accepted;
+import org.apache.qpid.proton.amqp.messaging.Modified;
+import org.apache.qpid.proton.amqp.messaging.Rejected;
+import org.apache.qpid.proton.amqp.messaging.Released;
+import org.apache.qpid.proton.amqp.messaging.Source;
+import org.apache.qpid.proton.amqp.messaging.Target;
+import org.apache.qpid.proton.amqp.messaging.TerminusDurability;
+import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy;
+import org.apache.qpid.proton.amqp.transaction.TransactionalState;
+import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
+import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.engine.Receiver;
+import org.apache.qpid.proton.message.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.activemq.transport.amqp.AmqpSupport.COPY;
+import static org.apache.activemq.transport.amqp.AmqpSupport.JMS_SELECTOR_NAME;
+import static org.apache.activemq.transport.amqp.AmqpSupport.NO_LOCAL_NAME;
+
+/**
+ * Receiver class that manages a Proton receiver endpoint.
+ */
+public class AmqpReceiver extends AmqpAbstractResource<Receiver> {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpReceiver.class);
+
+   private final AtomicBoolean closed = new AtomicBoolean();
+   private final BlockingQueue<AmqpMessage> prefetch = new LinkedBlockingDeque<>();
+
+   private final AmqpSession session;
+   private final String address;
+   private final String receiverId;
+   private final Source userSpecifiedSource;
+
+   private String subscriptionName;
+   private String selector;
+   private boolean presettle;
+   private boolean noLocal;
+
+   private AsyncResult pullRequest;
+   private AsyncResult stopRequest;
+
+   /**
+    * Create a new receiver instance.
+    *
+    * @param session    The parent session that created the receiver.
+    * @param address    The address that this receiver should listen on.
+    * @param receiverId The unique ID assigned to this receiver.
+    */
+   public AmqpReceiver(AmqpSession session, String address, String receiverId) {
+
+      if (address != null && address.isEmpty()) {
+         throw new IllegalArgumentException("Address cannot be empty.");
+      }
+
+      this.userSpecifiedSource = null;
+      this.session = session;
+      this.address = address;
+      this.receiverId = receiverId;
+   }
+
+   /**
+    * Create a new receiver instance.
+    *
+    * @param session    The parent session that created the receiver.
+    * @param source     The Source instance to use instead of creating and configuring one.
+    * @param receiverId The unique ID assigned to this receiver.
+    */
+   public AmqpReceiver(AmqpSession session, Source source, String receiverId) {
+
+      if (source == null) {
+         throw new IllegalArgumentException("User specified Source cannot be null");
+      }
+
+      this.session = session;
+      this.userSpecifiedSource = source;
+      this.address = source.getAddress();
+      this.receiverId = receiverId;
+   }
+
+   /**
+    * Close the receiver, a closed receiver will throw exceptions if any further send
+    * calls are made.
+    *
+    * @throws IOException if an error occurs while closing the receiver.
+    */
+   public void close() throws IOException {
+      if (closed.compareAndSet(false, true)) {
+         final ClientFuture request = new ClientFuture();
+         session.getScheduler().execute(new Runnable() {
+
+            @Override
+            public void run() {
+               checkClosed();
+               close(request);
+               session.pumpToProtonTransport(request);
+            }
+         });
+
+         request.sync();
+      }
+   }
+
+   /**
+    * Detach the receiver, a closed receiver will throw exceptions if any further send
+    * calls are made.
+    *
+    * @throws IOException if an error occurs while closing the receiver.
+    */
+   public void detach() throws IOException {
+      if (closed.compareAndSet(false, true)) {
+         final ClientFuture request = new ClientFuture();
+         session.getScheduler().execute(new Runnable() {
+
+            @Override
+            public void run() {
+               checkClosed();
+               detach(request);
+               session.pumpToProtonTransport(request);
+            }
+         });
+
+         request.sync();
+      }
+   }
+
+   /**
+    * @return this session's parent AmqpSession.
+    */
+   public AmqpSession getSession() {
+      return session;
+   }
+
+   /**
+    * @return the address that this receiver has been configured to listen on.
+    */
+   public String getAddress() {
+      return address;
+   }
+
+   /**
+    * Attempts to wait on a message to be delivered to this receiver.  The receive
+    * call will wait indefinitely for a message to be delivered.
+    *
+    * @return a newly received message sent to this receiver.
+    * @throws Exception if an error occurs during the receive attempt.
+    */
+   public AmqpMessage receive() throws Exception {
+      checkClosed();
+      return prefetch.take();
+   }
+
+   /**
+    * Attempts to receive a message sent to this receiver, waiting for the given
+    * timeout value before giving up and returning null.
+    *
+    * @param timeout the time to wait for a new message to arrive.
+    * @param unit    the unit of time that the timeout value represents.
+    * @return a newly received message or null if the time to wait period expires.
+    * @throws Exception if an error occurs during the receive attempt.
+    */
+   public AmqpMessage receive(long timeout, TimeUnit unit) throws Exception {
+      checkClosed();
+      return prefetch.poll(timeout, unit);
+   }
+
+   /**
+    * If a message is already available in this receiver's prefetch buffer then
+    * it is returned immediately otherwise this methods return null without waiting.
+    *
+    * @return a newly received message or null if there is no currently available message.
+    * @throws Exception if an error occurs during the receive attempt.
+    */
+   public AmqpMessage receiveNoWait() throws Exception {
+      checkClosed();
+      return prefetch.poll();
+   }
+
+   /**
+    * Request a remote peer send a Message to this client waiting until one arrives.
+    *
+    * @return the pulled AmqpMessage or null if none was pulled from the remote.
+    * @throws IOException if an error occurs
+    */
+   public AmqpMessage pull() throws IOException {
+      return pull(-1, TimeUnit.MILLISECONDS);
+   }
+
+   /**
+    * Request a remote peer send a Message to this client using an immediate drain request.
+    *
+    * @return the pulled AmqpMessage or null if none was pulled from the remote.
+    * @throws IOException if an error occurs
+    */
+   public AmqpMessage pullImmediate() throws IOException {
+      return pull(0, TimeUnit.MILLISECONDS);
+   }
+
+   /**
+    * Request a remote peer send a Message to this client.
+    *
+    * {@literal timeout < 0} then it should remain open until a message is received.
+    * {@literal timeout = 0} then it returns a message or null if none available
+    * {@literal timeout > 0} then it should remain open for timeout amount of time.
+    *
+    * The timeout value when positive is given in milliseconds.
+    *
+    * @param timeout the amount of time to tell the remote peer to keep this pull request valid.
+    * @param unit    the unit of measure that the timeout represents.
+    * @return the pulled AmqpMessage or null if none was pulled from the remote.
+    * @throws IOException if an error occurs
+    */
+   public AmqpMessage pull(final long timeout, final TimeUnit unit) throws IOException {
+      checkClosed();
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+
+            long timeoutMills = unit.toMillis(timeout);
+
+            try {
+               LOG.trace("Pull on Receiver {} with timeout = {}", getSubscriptionName(), timeoutMills);
+               if (timeoutMills < 0) {
+                  // Wait until message arrives. Just give credit if needed.
+                  if (getEndpoint().getCredit() == 0) {
+                     LOG.trace("Receiver {} granting 1 additional credit for pull.", getSubscriptionName());
+                     getEndpoint().flow(1);
+                  }
+
+                  // Await the message arrival
+                  pullRequest = request;
+               }
+               else if (timeoutMills == 0) {
+                  // If we have no credit then we need to issue some so that we can
+                  // try to fulfill the request, then drain down what is there to
+                  // ensure we consume what is available and remove all credit.
+                  if (getEndpoint().getCredit() == 0) {
+                     LOG.trace("Receiver {} granting 1 additional credit for pull.", getSubscriptionName());
+                     getEndpoint().flow(1);
+                  }
+
+                  // Drain immediately and wait for the message(s) to arrive,
+                  // or a flow indicating removal of the remaining credit.
+                  stop(request);
+               }
+               else if (timeoutMills > 0) {
+                  // If we have no credit then we need to issue some so that we can
+                  // try to fulfill the request, then drain down what is there to
+                  // ensure we consume what is available and remove all credit.
+                  if (getEndpoint().getCredit() == 0) {
+                     LOG.trace("Receiver {} granting 1 additional credit for pull.", getSubscriptionName());
+                     getEndpoint().flow(1);
+                  }
+
+                  // Wait for the timeout for the message(s) to arrive, then drain if required
+                  // and wait for remaining message(s) to arrive or a flow indicating
+                  // removal of the remaining credit.
+                  stopOnSchedule(timeoutMills, request);
+               }
+
+               session.pumpToProtonTransport(request);
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+
+      return prefetch.poll();
+   }
+
+   /**
+    * Controls the amount of credit given to the receiver link.
+    *
+    * @param credit the amount of credit to grant.
+    * @throws IOException if an error occurs while sending the flow.
+    */
+   public void flow(final int credit) throws IOException {
+      checkClosed();
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               getEndpoint().flow(credit);
+               session.pumpToProtonTransport(request);
+               request.onSuccess();
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Attempts to drain a given amount of credit from the link.
+    *
+    * @param credit the amount of credit to drain.
+    * @throws IOException if an error occurs while sending the drain.
+    */
+   public void drain(final int credit) throws IOException {
+      checkClosed();
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               getEndpoint().drain(credit);
+               session.pumpToProtonTransport(request);
+               request.onSuccess();
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Stops the receiver, using all link credit and waiting for in-flight messages to arrive.
+    *
+    * @throws IOException if an error occurs while sending the drain.
+    */
+   public void stop() throws IOException {
+      checkClosed();
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               stop(request);
+               session.pumpToProtonTransport(request);
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Accepts a message that was dispatched under the given Delivery instance.
+    *
+    * @param delivery the Delivery instance to accept.
+    * @throws IOException if an error occurs while sending the accept.
+    */
+   public void accept(final Delivery delivery) throws IOException {
+      checkClosed();
+
+      if (delivery == null) {
+         throw new IllegalArgumentException("Delivery to accept cannot be null");
+      }
+
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               if (!delivery.isSettled()) {
+                  if (session.isInTransaction()) {
+                     Binary txnId = session.getTransactionId().getRemoteTxId();
+                     if (txnId != null) {
+                        TransactionalState txState = new TransactionalState();
+                        txState.setOutcome(Accepted.getInstance());
+                        txState.setTxnId(txnId);
+                        delivery.disposition(txState);
+                        delivery.settle();
+                        session.getTransactionContext().registerTxConsumer(AmqpReceiver.this);
+                     }
+                  }
+                  else {
+                     delivery.disposition(Accepted.getInstance());
+                     delivery.settle();
+                  }
+               }
+               session.pumpToProtonTransport(request);
+               request.onSuccess();
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Mark a message that was dispatched under the given Delivery instance as Modified.
+    *
+    * @param delivery          the Delivery instance to mark modified.
+    * @param deliveryFailed    indicates that the delivery failed for some reason.
+    * @param undeliverableHere marks the delivery as not being able to be process by link it was sent to.
+    * @throws IOException if an error occurs while sending the reject.
+    */
+   public void modified(final Delivery delivery,
+                        final Boolean deliveryFailed,
+                        final Boolean undeliverableHere) throws IOException {
+      checkClosed();
+
+      if (delivery == null) {
+         throw new IllegalArgumentException("Delivery to reject cannot be null");
+      }
+
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               if (!delivery.isSettled()) {
+                  Modified disposition = new Modified();
+                  disposition.setUndeliverableHere(undeliverableHere);
+                  disposition.setDeliveryFailed(deliveryFailed);
+                  delivery.disposition(disposition);
+                  delivery.settle();
+                  session.pumpToProtonTransport(request);
+               }
+               request.onSuccess();
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * Release a message that was dispatched under the given Delivery instance.
+    *
+    * @param delivery the Delivery instance to release.
+    * @throws IOException if an error occurs while sending the release.
+    */
+   public void release(final Delivery delivery) throws IOException {
+      checkClosed();
+
+      if (delivery == null) {
+         throw new IllegalArgumentException("Delivery to release cannot be null");
+      }
+
+      final ClientFuture request = new ClientFuture();
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            checkClosed();
+            try {
+               if (!delivery.isSettled()) {
+                  delivery.disposition(Released.getInstance());
+                  delivery.settle();
+                  session.pumpToProtonTransport(request);
+               }
+               request.onSuccess();
+            }
+            catch (Exception e) {
+               request.onFailure(e);
+            }
+         }
+      });
+
+      request.sync();
+   }
+
+   /**
+    * @return an unmodifiable view of the underlying Receiver instance.
+    */
+   public Receiver getReceiver() {
+      return new UnmodifiableReceiver(getEndpoint());
+   }
+
+   //----- Receiver configuration properties --------------------------------//
+
+   public boolean isPresettle() {
+      return presettle;
+   }
+
+   public void setPresettle(boolean presettle) {
+      this.presettle = presettle;
+   }
+
+   public boolean isDurable() {
+      return subscriptionName != null;
+   }
+
+   public String getSubscriptionName() {
+      return subscriptionName;
+   }
+
+   public void setSubscriptionName(String subscriptionName) {
+      this.subscriptionName = subscriptionName;
+   }
+
+   public String getSelector() {
+      return selector;
+   }
+
+   public void setSelector(String selector) {
+      this.selector = selector;
+   }
+
+   public boolean isNoLocal() {
+      return noLocal;
+   }
+
+   public void setNoLocal(boolean noLocal) {
+      this.noLocal = noLocal;
+   }
+
+   public long getDrainTimeout() {
+      return session.getConnection().getDrainTimeout();
+   }
+
+   //----- Internal implementation ------------------------------------------//
+
+   @Override
+   protected void doOpen() {
+
+      Source source = userSpecifiedSource;
+      Target target = new Target();
+
+      if (source == null && address != null) {
+         source = new Source();
+         source.setAddress(address);
+         configureSource(source);
+      }
+
+      String receiverName = receiverId + ":" + address;
+
+      if (getSubscriptionName() != null && !getSubscriptionName().isEmpty()) {
+         // In the case of Durable Topic Subscriptions the client must use the same
+         // receiver name which is derived from the subscription name property.
+         receiverName = getSubscriptionName();
+      }
+
+      Receiver receiver = session.getEndpoint().receiver(receiverName);
+      receiver.setSource(source);
+      receiver.setTarget(target);
+      if (isPresettle()) {
+         receiver.setSenderSettleMode(SenderSettleMode.SETTLED);
+      }
+      else {
+         receiver.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+      }
+      receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+
+      setEndpoint(receiver);
+
+      super.doOpen();
+   }
+
+   @Override
+   protected void doOpenCompletion() {
+      // Verify the attach response contained a non-null Source
+      org.apache.qpid.proton.amqp.transport.Source s = getEndpoint().getRemoteSource();
+      if (s != null) {
+         super.doOpenCompletion();
+      }
+      else {
+         // No link terminus was created, the peer will now detach/close us.
+      }
+   }
+
+   @Override
+   protected void doClose() {
+      getEndpoint().close();
+   }
+
+   @Override
+   protected void doDetach() {
+      getEndpoint().detach();
+   }
+
+   @Override
+   protected Exception getOpenAbortException() {
+      // Verify the attach response contained a non-null Source
+      org.apache.qpid.proton.amqp.transport.Source s = getEndpoint().getRemoteSource();
+      if (s != null) {
+         return super.getOpenAbortException();
+      }
+      else {
+         // No link terminus was created, the peer has detach/closed us, create IDE.
+         return new InvalidDestinationException("Link creation was refused");
+      }
+   }
+
+   @Override
+   protected void doOpenInspection() {
+      try {
+         getStateInspector().inspectOpenedResource(getReceiver());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected void doClosedInspection() {
+      try {
+         getStateInspector().inspectClosedResource(getReceiver());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected void doDetachedInspection() {
+      try {
+         getStateInspector().inspectDetachedResource(getReceiver());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   protected void configureSource(Source source) {
+      Map<Symbol, DescribedType> filters = new HashMap<>();
+      Symbol[] outcomes = new Symbol[]{Accepted.DESCRIPTOR_SYMBOL, Rejected.DESCRIPTOR_SYMBOL, Released.DESCRIPTOR_SYMBOL, Modified.DESCRIPTOR_SYMBOL};
+
+      if (getSubscriptionName() != null && !getSubscriptionName().isEmpty()) {
+         source.setExpiryPolicy(TerminusExpiryPolicy.NEVER);
+         source.setDurable(TerminusDurability.UNSETTLED_STATE);
+         source.setDistributionMode(COPY);
+      }
+      else {
+         source.setDurable(TerminusDurability.NONE);
+         source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH);
+      }
+
+      source.setOutcomes(outcomes);
+
+      Modified modified = new Modified();
+      modified.setDeliveryFailed(true);
+      modified.setUndeliverableHere(false);
+
+      source.setDefaultOutcome(modified);
+
+      if (isNoLocal()) {
+         filters.put(NO_LOCAL_NAME, AmqpNoLocalFilter.NO_LOCAL);
+      }
+
+      if (getSelector() != null && !getSelector().trim().equals("")) {
+         filters.put(JMS_SELECTOR_NAME, new AmqpJmsSelectorFilter(getSelector()));
+      }
+
+      if (!filters.isEmpty()) {
+         source.setFilter(filters);
+      }
+   }
+
+   @Override
+   public void processDeliveryUpdates(AmqpConnection connection) throws IOException {
+      Delivery incoming = null;
+      do {
+         incoming = getEndpoint().current();
+         if (incoming != null) {
+            if (incoming.isReadable() && !incoming.isPartial()) {
+               LOG.trace("{} has incoming Message(s).", this);
+               try {
+                  processDelivery(incoming);
+               }
+               catch (Exception e) {
+                  throw IOExceptionSupport.create(e);
+               }
+               getEndpoint().advance();
+            }
+            else {
+               LOG.trace("{} has a partial incoming Message(s), deferring.", this);
+               incoming = null;
+            }
+         }
+         else {
+            // We have exhausted the locally queued messages on this link.
+            // Check if we tried to stop and have now run out of credit.
+            if (getEndpoint().getRemoteCredit() <= 0) {
+               if (stopRequest != null) {
+                  stopRequest.onSuccess();
+                  stopRequest = null;
+               }
+            }
+         }
+      } while (incoming != null);
+
+      super.processDeliveryUpdates(connection);
+   }
+
+   private void processDelivery(Delivery incoming) throws Exception {
+      Message message = null;
+      try {
+         message = decodeIncomingMessage(incoming);
+      }
+      catch (Exception e) {
+         LOG.warn("Error on transform: {}", e.getMessage());
+         deliveryFailed(incoming, true);
+         return;
+      }
+
+      AmqpMessage amqpMessage = new AmqpMessage(this, message, incoming);
+      // Store reference to envelope in delivery context for recovery
+      incoming.setContext(amqpMessage);
+      prefetch.add(amqpMessage);
+
+      // We processed a message, signal completion
+      // of a message pull request if there is one.
+      if (pullRequest != null) {
+         pullRequest.onSuccess();
+         pullRequest = null;
+      }
+   }
+
+   @Override
+   public void processFlowUpdates(AmqpConnection connection) throws IOException {
+      if (pullRequest != null || stopRequest != null) {
+         Receiver receiver = getEndpoint();
+         if (receiver.getRemoteCredit() <= 0 && receiver.getQueued() == 0) {
+            if (pullRequest != null) {
+               pullRequest.onSuccess();
+               pullRequest = null;
+            }
+
+            if (stopRequest != null) {
+               stopRequest.onSuccess();
+               stopRequest = null;
+            }
+         }
+      }
+
+      LOG.trace("Consumer {} flow updated, remote credit = {}", getSubscriptionName(), getEndpoint().getRemoteCredit());
+
+      super.processFlowUpdates(connection);
+   }
+
+   protected Message decodeIncomingMessage(Delivery incoming) {
+      int count;
+
+      byte[] chunk = new byte[2048];
+      ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+      while ((count = getEndpoint().recv(chunk, 0, chunk.length)) > 0) {
+         stream.write(chunk, 0, count);
+      }
+
+      byte[] messageBytes = stream.toByteArray();
+
+      try {
+         Message protonMessage = Message.Factory.create();
+         protonMessage.decode(messageBytes, 0, messageBytes.length);
+         return protonMessage;
+      }
+      finally {
+         try {
+            stream.close();
+         }
+         catch (IOException e) {
+         }
+      }
+   }
+
+   protected void deliveryFailed(Delivery incoming, boolean expandCredit) {
+      Modified disposition = new Modified();
+      disposition.setUndeliverableHere(true);
+      disposition.setDeliveryFailed(true);
+      incoming.disposition(disposition);
+      incoming.settle();
+      if (expandCredit) {
+         getEndpoint().flow(1);
+      }
+   }
+
+   private void stop(final AsyncResult request) {
+      Receiver receiver = getEndpoint();
+      if (receiver.getRemoteCredit() <= 0) {
+         if (receiver.getQueued() == 0) {
+            // We have no remote credit and all the deliveries have been processed.
+            request.onSuccess();
+         }
+         else {
+            // There are still deliveries to process, wait for them to be.
+            stopRequest = request;
+         }
+      }
+      else {
+         // TODO: We don't actually want the additional messages that could be sent while
+         // draining. We could explicitly reduce credit first, or possibly use 'echo' instead
+         // of drain if it was supported. We would first need to understand what happens
+         // if we reduce credit below the number of messages already in-flight before
+         // the peer sees the update.
+         stopRequest = request;
+         receiver.drain(0);
+
+         if (getDrainTimeout() > 0) {
+            // If the remote doesn't respond we will close the consumer and break any
+            // blocked receive or stop calls that are waiting.
+            final ScheduledFuture<?> future = getSession().getScheduler().schedule(new Runnable() {
+               @Override
+               public void run() {
+                  LOG.trace("Consumer {} drain request timed out", this);
+                  Exception cause = new JmsOperationTimedOutException("Remote did not respond to a drain request in time");
+                  locallyClosed(session.getConnection(), cause);
+                  stopRequest.onFailure(cause);
+                  session.pumpToProtonTransport(stopRequest);
+               }
+            }, getDrainTimeout(), TimeUnit.MILLISECONDS);
+
+            stopRequest = new ScheduledRequest(future, stopRequest);
+         }
+      }
+   }
+
+   private void stopOnSchedule(long timeout, final AsyncResult request) {
+      LOG.trace("Receiver {} scheduling stop", this);
+      // We need to drain the credit if no message(s) arrive to use it.
+      final ScheduledFuture<?> future = getSession().getScheduler().schedule(new Runnable() {
+         @Override
+         public void run() {
+            LOG.trace("Receiver {} running scheduled stop", this);
+            if (getEndpoint().getRemoteCredit() != 0) {
+               stop(request);
+               session.pumpToProtonTransport(request);
+            }
+         }
+      }, timeout, TimeUnit.MILLISECONDS);
+
+      stopRequest = new ScheduledRequest(future, request);
+   }
+
+   @Override
+   public String toString() {
+      return getClass().getSimpleName() + "{ address = " + address + "}";
+   }
+
+   private void checkClosed() {
+      if (isClosed()) {
+         throw new IllegalStateException("Receiver is already closed");
+      }
+   }
+
+   //----- Internal Transaction state callbacks -----------------------------//
+
+   void preCommit() {
+   }
+
+   void preRollback() {
+   }
+
+   void postCommit() {
+   }
+
+   void postRollback() {
+   }
+
+   //----- Inner classes used in message pull operations --------------------//
+
+   protected static final class ScheduledRequest implements AsyncResult {
+
+      private final ScheduledFuture<?> sheduledTask;
+      private final AsyncResult origRequest;
+
+      public ScheduledRequest(ScheduledFuture<?> completionTask, AsyncResult origRequest) {
+         this.sheduledTask = completionTask;
+         this.origRequest = origRequest;
+      }
+
+      @Override
+      public void onFailure(Throwable cause) {
+         sheduledTask.cancel(false);
+         origRequest.onFailure(cause);
+      }
+
+      @Override
+      public void onSuccess() {
+         boolean cancelled = sheduledTask.cancel(false);
+         if (cancelled) {
+            // Signal completion. Otherwise wait for the scheduled task to do it.
+            origRequest.onSuccess();
+         }
+      }
+
+      @Override
+      public boolean isComplete() {
+         return origRequest.isComplete();
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpRedirectedException.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpRedirectedException.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpRedirectedException.java
new file mode 100644
index 0000000..0c9bb81
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpRedirectedException.java
@@ -0,0 +1,61 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import java.io.IOException;
+
+/**
+ * {@link IOException} derivative that defines that the remote peer has requested that this
+ * connection be redirected to some alternative peer.
+ */
+public class AmqpRedirectedException extends IOException {
+
+   private static final long serialVersionUID = 5872211116061710369L;
+
+   private final String hostname;
+   private final String networkHost;
+   private final int port;
+
+   public AmqpRedirectedException(String reason, String hostname, String networkHost, int port) {
+      super(reason);
+
+      this.hostname = hostname;
+      this.networkHost = networkHost;
+      this.port = port;
+   }
+
+   /**
+    * @return the host name of the container being redirected to.
+    */
+   public String getHostname() {
+      return hostname;
+   }
+
+   /**
+    * @return the DNS host name or IP address of the peer this connection is being redirected to.
+    */
+   public String getNetworkHost() {
+      return networkHost;
+   }
+
+   /**
+    * @return the port number on the peer this connection is being redirected to.
+    */
+   public int getPort() {
+      return port;
+   }
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpResource.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpResource.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpResource.java
new file mode 100644
index 0000000..bd66659
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpResource.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.activemq.transport.amqp.client;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+
+/**
+ * AmqpResource specification.
+ *
+ * All AMQP types should implement this interface to allow for control of state
+ * and configuration details.
+ */
+public interface AmqpResource extends AmqpEventSink {
+
+   /**
+    * Perform all the work needed to open this resource and store the request
+    * until such time as the remote peer indicates the resource has become active.
+    *
+    * @param request The initiating request that triggered this open call.
+    */
+   void open(AsyncResult request);
+
+   /**
+    * @return if the resource has moved to the opened state on the remote.
+    */
+   boolean isOpen();
+
+   /**
+    * Called to indicate that this resource is now remotely opened.  Once opened a
+    * resource can start accepting incoming requests.
+    */
+   void opened();
+
+   /**
+    * Perform all work needed to close this resource and store the request
+    * until such time as the remote peer indicates the resource has been closed.
+    *
+    * @param request The initiating request that triggered this close call.
+    */
+   void close(AsyncResult request);
+
+   /**
+    * Perform all work needed to detach this resource and store the request
+    * until such time as the remote peer indicates the resource has been detached.
+    *
+    * @param request The initiating request that triggered this detach call.
+    */
+   void detach(AsyncResult request);
+
+   /**
+    * @return if the resource has moved to the closed state on the remote.
+    */
+   boolean isClosed();
+
+   /**
+    * Called to indicate that this resource is now remotely closed.  Once closed a
+    * resource can not accept any incoming requests.
+    */
+   void closed();
+
+   /**
+    * Sets the failed state for this Resource and triggers a failure signal for
+    * any pending ProduverRequest.
+    */
+   void failed();
+
+   /**
+    * Called to indicate that the remote end has become closed but the resource
+    * was not awaiting a close.  This could happen during an open request where
+    * the remote does not set an error condition or during normal operation.
+    *
+    * @param connection The connection that owns this resource.
+    */
+   void remotelyClosed(AmqpConnection connection);
+
+   /**
+    * Called to indicate that the local end has become closed but the resource
+    * was not awaiting a close.  This could happen during an open request where
+    * the remote does not set an error condition or during normal operation.
+    *
+    * @param connection The connection that owns this resource.
+    * @param error      The error that triggered the local close of this resource.
+    */
+   void locallyClosed(AmqpConnection connection, Exception error);
+
+   /**
+    * Sets the failed state for this Resource and triggers a failure signal for
+    * any pending ProduverRequest.
+    *
+    * @param cause The Exception that triggered the failure.
+    */
+   void failed(Exception cause);
+
+}

http://git-wip-us.apache.org/repos/asf/activemq-artemis/blob/df41a60e/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSender.java
----------------------------------------------------------------------
diff --git a/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSender.java b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSender.java
new file mode 100644
index 0000000..404b943
--- /dev/null
+++ b/tests/artemis-test-support/src/main/java/org/apache/activemq/transport/amqp/client/AmqpSender.java
@@ -0,0 +1,452 @@
+/**
+ * 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.activemq.transport.amqp.client;
+
+import javax.jms.InvalidDestinationException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.activemq.transport.amqp.client.util.AsyncResult;
+import org.apache.activemq.transport.amqp.client.util.ClientFuture;
+import org.apache.activemq.transport.amqp.client.util.UnmodifiableSender;
+import org.apache.qpid.proton.amqp.Binary;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.apache.qpid.proton.amqp.messaging.Accepted;
+import org.apache.qpid.proton.amqp.messaging.Modified;
+import org.apache.qpid.proton.amqp.messaging.Outcome;
+import org.apache.qpid.proton.amqp.messaging.Rejected;
+import org.apache.qpid.proton.amqp.messaging.Released;
+import org.apache.qpid.proton.amqp.messaging.Source;
+import org.apache.qpid.proton.amqp.messaging.Target;
+import org.apache.qpid.proton.amqp.transaction.TransactionalState;
+import org.apache.qpid.proton.amqp.transport.DeliveryState;
+import org.apache.qpid.proton.amqp.transport.ErrorCondition;
+import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
+import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
+import org.apache.qpid.proton.engine.Delivery;
+import org.apache.qpid.proton.engine.Sender;
+import org.apache.qpid.proton.message.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Sender class that manages a Proton sender endpoint.
+ */
+public class AmqpSender extends AmqpAbstractResource<Sender> {
+
+   private static final Logger LOG = LoggerFactory.getLogger(AmqpSender.class);
+   private static final byte[] EMPTY_BYTE_ARRAY = new byte[]{};
+
+   public static final long DEFAULT_SEND_TIMEOUT = 15000;
+
+   private final AmqpTransferTagGenerator tagGenerator = new AmqpTransferTagGenerator(true);
+   private final AtomicBoolean closed = new AtomicBoolean();
+
+   private final AmqpSession session;
+   private final String address;
+   private final String senderId;
+   private final Target userSpecifiedTarget;
+
+   private boolean presettle;
+   private long sendTimeout = DEFAULT_SEND_TIMEOUT;
+
+   private final Set<Delivery> pending = new LinkedHashSet<>();
+   private byte[] encodeBuffer = new byte[1024 * 8];
+
+   /**
+    * Create a new sender instance.
+    *
+    * @param session  The parent session that created the session.
+    * @param address  The address that this sender produces to.
+    * @param senderId The unique ID assigned to this sender.
+    */
+   public AmqpSender(AmqpSession session, String address, String senderId) {
+
+      if (address != null && address.isEmpty()) {
+         throw new IllegalArgumentException("Address cannot be empty.");
+      }
+
+      this.session = session;
+      this.address = address;
+      this.senderId = senderId;
+      this.userSpecifiedTarget = null;
+   }
+
+   /**
+    * Create a new sender instance using the given Target when creating the link.
+    *
+    * @param session  The parent session that created the session.
+    * @param address  The address that this sender produces to.
+    * @param senderId The unique ID assigned to this sender.
+    */
+   public AmqpSender(AmqpSession session, Target target, String senderId) {
+
+      if (target == null) {
+         throw new IllegalArgumentException("User specified Target cannot be null");
+      }
+
+      this.session = session;
+      this.userSpecifiedTarget = target;
+      this.address = target.getAddress();
+      this.senderId = senderId;
+   }
+
+   /**
+    * Sends the given message to this senders assigned address.
+    *
+    * @param message the message to send.
+    * @throws IOException if an error occurs during the send.
+    */
+   public void send(final AmqpMessage message) throws IOException {
+      checkClosed();
+      final ClientFuture sendRequest = new ClientFuture();
+
+      session.getScheduler().execute(new Runnable() {
+
+         @Override
+         public void run() {
+            try {
+               doSend(message, sendRequest);
+               session.pumpToProtonTransport(sendRequest);
+            }
+            catch (Exception e) {
+               sendRequest.onFailure(e);
+               session.getConnection().fireClientException(e);
+            }
+         }
+      });
+
+      if (sendTimeout <= 0) {
+         sendRequest.sync();
+      }
+      else {
+         sendRequest.sync(sendTimeout, TimeUnit.MILLISECONDS);
+      }
+   }
+
+   /**
+    * Close the sender, a closed sender will throw exceptions if any further send
+    * calls are made.
+    *
+    * @throws IOException if an error occurs while closing the sender.
+    */
+   public void close() throws IOException {
+      if (closed.compareAndSet(false, true)) {
+         final ClientFuture request = new ClientFuture();
+         session.getScheduler().execute(new Runnable() {
+
+            @Override
+            public void run() {
+               checkClosed();
+               close(request);
+               session.pumpToProtonTransport(request);
+            }
+         });
+
+         request.sync();
+      }
+   }
+
+   /**
+    * @return this session's parent AmqpSession.
+    */
+   public AmqpSession getSession() {
+      return session;
+   }
+
+   /**
+    * @return an unmodifiable view of the underlying Sender instance.
+    */
+   public Sender getSender() {
+      return new UnmodifiableSender(getEndpoint());
+   }
+
+   /**
+    * @return the assigned address of this sender.
+    */
+   public String getAddress() {
+      return address;
+   }
+
+   //----- Sender configuration ---------------------------------------------//
+
+   /**
+    * @return will messages be settle on send.
+    */
+   public boolean isPresettle() {
+      return presettle;
+   }
+
+   /**
+    * Configure is sent messages are marked as settled on send, defaults to false.
+    *
+    * @param presettle configure if this sender will presettle all sent messages.
+    */
+   public void setPresettle(boolean presettle) {
+      this.presettle = presettle;
+   }
+
+   /**
+    * @return the currently configured send timeout.
+    */
+   public long getSendTimeout() {
+      return sendTimeout;
+   }
+
+   /**
+    * Sets the amount of time the sender will block on a send before failing.
+    *
+    * @param sendTimeout time in milliseconds to wait.
+    */
+   public void setSendTimeout(long sendTimeout) {
+      this.sendTimeout = sendTimeout;
+   }
+
+   //----- Private Sender implementation ------------------------------------//
+
+   private void checkClosed() {
+      if (isClosed()) {
+         throw new IllegalStateException("Sender is already closed");
+      }
+   }
+
+   @Override
+   protected void doOpen() {
+
+      Symbol[] outcomes = new Symbol[]{Accepted.DESCRIPTOR_SYMBOL, Rejected.DESCRIPTOR_SYMBOL};
+      Source source = new Source();
+      source.setAddress(senderId);
+      source.setOutcomes(outcomes);
+
+      Target target = userSpecifiedTarget;
+      if (target == null) {
+         target = new Target();
+         target.setAddress(address);
+      }
+
+      String senderName = senderId + ":" + address;
+
+      Sender sender = session.getEndpoint().sender(senderName);
+      sender.setSource(source);
+      sender.setTarget(target);
+      if (presettle) {
+         sender.setSenderSettleMode(SenderSettleMode.SETTLED);
+      }
+      else {
+         sender.setSenderSettleMode(SenderSettleMode.UNSETTLED);
+      }
+      sender.setReceiverSettleMode(ReceiverSettleMode.FIRST);
+
+      setEndpoint(sender);
+
+      super.doOpen();
+   }
+
+   @Override
+   protected void doOpenCompletion() {
+      // Verify the attach response contained a non-null target
+      org.apache.qpid.proton.amqp.transport.Target t = getEndpoint().getRemoteTarget();
+      if (t != null) {
+         super.doOpenCompletion();
+      }
+      else {
+         // No link terminus was created, the peer will now detach/close us.
+      }
+   }
+
+   @Override
+   protected void doOpenInspection() {
+      try {
+         getStateInspector().inspectOpenedResource(getSender());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected void doClosedInspection() {
+      try {
+         getStateInspector().inspectClosedResource(getSender());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected void doDetachedInspection() {
+      try {
+         getStateInspector().inspectDetachedResource(getSender());
+      }
+      catch (Throwable error) {
+         getStateInspector().markAsInvalid(error.getMessage());
+      }
+   }
+
+   @Override
+   protected Exception getOpenAbortException() {
+      // Verify the attach response contained a non-null target
+      org.apache.qpid.proton.amqp.transport.Target t = getEndpoint().getRemoteTarget();
+      if (t != null) {
+         return super.getOpenAbortException();
+      }
+      else {
+         // No link terminus was created, the peer has detach/closed us, create IDE.
+         return new InvalidDestinationException("Link creation was refused");
+      }
+   }
+
+   private void doSend(AmqpMessage message, AsyncResult request) throws Exception {
+      LOG.trace("Producer sending message: {}", message);
+
+      Delivery delivery = null;
+      if (presettle) {
+         delivery = getEndpoint().delivery(EMPTY_BYTE_ARRAY, 0, 0);
+      }
+      else {
+         byte[] tag = tagGenerator.getNextTag();
+         delivery = getEndpoint().delivery(tag, 0, tag.length);
+      }
+
+      delivery.setContext(request);
+
+      if (session.isInTransaction()) {
+         Binary amqpTxId = session.getTransactionId().getRemoteTxId();
+         TransactionalState state = new TransactionalState();
+         state.setTxnId(amqpTxId);
+         delivery.disposition(state);
+      }
+
+      encodeAndSend(message.getWrappedMessage(), delivery);
+
+      if (presettle) {
+         delivery.settle();
+         request.onSuccess();
+      }
+      else {
+         pending.add(delivery);
+         getEndpoint().advance();
+      }
+   }
+
+   private void encodeAndSend(Message message, Delivery delivery) throws IOException {
+
+      int encodedSize;
+      while (true) {
+         try {
+            encodedSize = message.encode(encodeBuffer, 0, encodeBuffer.length);
+            break;
+         }
+         catch (java.nio.BufferOverflowException e) {
+            encodeBuffer = new byte[encodeBuffer.length * 2];
+         }
+      }
+
+      int sentSoFar = 0;
+
+      while (true) {
+         int sent = getEndpoint().send(encodeBuffer, sentSoFar, encodedSize - sentSoFar);
+         if (sent > 0) {
+            sentSoFar += sent;
+            if ((encodedSize - sentSoFar) == 0) {
+               break;
+            }
+         }
+         else {
+            LOG.warn("{} failed to send any data from current Message.", this);
+         }
+      }
+   }
+
+   @Override
+   public void processDeliveryUpdates(AmqpConnection connection) throws IOException {
+      List<Delivery> toRemove = new ArrayList<>();
+
+      for (Delivery delivery : pending) {
+         DeliveryState state = delivery.getRemoteState();
+         if (state == null) {
+            continue;
+         }
+
+         Outcome outcome = null;
+         if (state instanceof TransactionalState) {
+            LOG.trace("State of delivery is Transactional, retrieving outcome: {}", state);
+            outcome = ((TransactionalState) state).getOutcome();
+         }
+         else if (state instanceof Outcome) {
+            outcome = (Outcome) state;
+         }
+         else {
+            LOG.warn("Message send updated with unsupported state: {}", state);
+            outcome = null;
+         }
+
+         AsyncResult request = (AsyncResult) delivery.getContext();
+         Exception deliveryError = null;
+
+         if (outcome instanceof Accepted) {
+            LOG.trace("Outcome of delivery was accepted: {}", delivery);
+            if (request != null && !request.isComplete()) {
+               request.onSuccess();
+            }
+         }
+         else if (outcome instanceof Rejected) {
+            LOG.trace("Outcome of delivery was rejected: {}", delivery);
+            ErrorCondition remoteError = ((Rejected) outcome).getError();
+            if (remoteError == null) {
+               remoteError = getEndpoint().getRemoteCondition();
+            }
+
+            deliveryError = AmqpSupport.convertToException(remoteError);
+         }
+         else if (outcome instanceof Released) {
+            LOG.trace("Outcome of delivery was released: {}", delivery);
+            deliveryError = new IOException("Delivery failed: released by receiver");
+         }
+         else if (outcome instanceof Modified) {
+            LOG.trace("Outcome of delivery was modified: {}", delivery);
+            deliveryError = new IOException("Delivery failed: failure at remote");
+         }
+
+         if (deliveryError != null) {
+            if (request != null && !request.isComplete()) {
+               request.onFailure(deliveryError);
+            }
+            else {
+               connection.fireClientException(deliveryError);
+            }
+         }
+
+         tagGenerator.returnTag(delivery.getTag());
+         delivery.settle();
+         toRemove.add(delivery);
+      }
+
+      pending.removeAll(toRemove);
+   }
+
+   @Override
+   public String toString() {
+      return getClass().getSimpleName() + "{ address = " + address + "}";
+   }
+}