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: <hostname>: <hostname>
+ * 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 + "}";
+ }
+}