This closes #642 Flow control improvements

This commit is contained in:
Andy Taylor 2016-07-20 10:33:44 +01:00
commit 413e7aee54
70 changed files with 11030 additions and 40 deletions

View File

@ -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();
}
@ -378,6 +396,24 @@ public class ProtonSessionIntegrationCallback implements AMQPSessionCallback, Se
return manager.getPubSubPrefix();
}
@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));

View File

@ -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;

View File

@ -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 {

View File

@ -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);

View File

@ -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);
}
}

View File

@ -69,7 +69,6 @@ public class ProtonServerConnectionContext extends AbstractConnectionContext imp
}
else {
protonSession.addReceiver(receiver);
receiver.flow(100);
}
}
else {

View File

@ -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 {

View File

@ -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;

View File

@ -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 {
@ -75,6 +75,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 {

View File

@ -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,37 @@ 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.
#### 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,

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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 + " }";
}
}

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -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;
}

View File

@ -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 + "}";
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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 + "}";
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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];
}
};
}
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -0,0 +1,274 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.transport.amqp.client.util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Generator for Globally unique Strings.
*/
public class IdGenerator {
private static final Logger LOG = LoggerFactory.getLogger(IdGenerator.class);
private static final String UNIQUE_STUB;
private static int instanceCount;
private static String hostName;
private String seed;
private final AtomicLong sequence = new AtomicLong(1);
private int length;
public static final String PROPERTY_IDGENERATOR_PORT = "activemq.idgenerator.port";
static {
String stub = "";
boolean canAccessSystemProps = true;
try {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPropertiesAccess();
}
}
catch (SecurityException se) {
canAccessSystemProps = false;
}
if (canAccessSystemProps) {
int idGeneratorPort = 0;
ServerSocket ss = null;
try {
idGeneratorPort = Integer.parseInt(System.getProperty(PROPERTY_IDGENERATOR_PORT, "0"));
LOG.trace("Using port {}", idGeneratorPort);
hostName = getLocalHostName();
ss = new ServerSocket(idGeneratorPort);
stub = "-" + ss.getLocalPort() + "-" + System.currentTimeMillis() + "-";
Thread.sleep(100);
}
catch (Exception e) {
if (LOG.isTraceEnabled()) {
LOG.trace("could not generate unique stub by using DNS and binding to local port", e);
}
else {
LOG.warn("could not generate unique stub by using DNS and binding to local port: {} {}", e.getClass().getCanonicalName(), e.getMessage());
}
// Restore interrupted state so higher level code can deal with it.
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
finally {
if (ss != null) {
try {
ss.close();
}
catch (IOException ioe) {
if (LOG.isTraceEnabled()) {
LOG.trace("Closing the server socket failed", ioe);
}
else {
LOG.warn("Closing the server socket failed" + " due " + ioe.getMessage());
}
}
}
}
}
if (hostName == null) {
hostName = "localhost";
}
hostName = sanitizeHostName(hostName);
if (stub.length() == 0) {
stub = "-1-" + System.currentTimeMillis() + "-";
}
UNIQUE_STUB = stub;
}
/**
* Construct an IdGenerator
*
* @param prefix The prefix value that is applied to all generated IDs.
*/
public IdGenerator(String prefix) {
synchronized (UNIQUE_STUB) {
this.seed = prefix + UNIQUE_STUB + (instanceCount++) + ":";
this.length = this.seed.length() + ("" + Long.MAX_VALUE).length();
}
}
public IdGenerator() {
this("ID:" + hostName);
}
/**
* As we have to find the host name as a side-affect of generating a unique stub, we allow
* it's easy retrieval here
*
* @return the local host name
*/
public static String getHostName() {
return hostName;
}
/**
* Generate a unique id
*
* @return a unique id
*/
public synchronized String generateId() {
StringBuilder sb = new StringBuilder(length);
sb.append(seed);
sb.append(sequence.getAndIncrement());
return sb.toString();
}
public static String sanitizeHostName(String hostName) {
boolean changed = false;
StringBuilder sb = new StringBuilder();
for (char ch : hostName.toCharArray()) {
// only include ASCII chars
if (ch < 127) {
sb.append(ch);
}
else {
changed = true;
}
}
if (changed) {
String newHost = sb.toString();
LOG.info("Sanitized hostname from: {} to: {}", hostName, newHost);
return newHost;
}
else {
return hostName;
}
}
/**
* Generate a unique ID - that is friendly for a URL or file system
*
* @return a unique id
*/
public String generateSanitizedId() {
String result = generateId();
result = result.replace(':', '-');
result = result.replace('_', '-');
result = result.replace('.', '-');
return result;
}
/**
* From a generated id - return the seed (i.e. minus the count)
*
* @param id the generated identifier
* @return the seed
*/
public static String getSeedFromId(String id) {
String result = id;
if (id != null) {
int index = id.lastIndexOf(':');
if (index > 0 && (index + 1) < id.length()) {
result = id.substring(0, index);
}
}
return result;
}
/**
* From a generated id - return the generator count
*
* @param id The ID that will be parsed for a sequence number.
* @return the sequence value parsed from the given ID.
*/
public static long getSequenceFromId(String id) {
long result = -1;
if (id != null) {
int index = id.lastIndexOf(':');
if (index > 0 && (index + 1) < id.length()) {
String numStr = id.substring(index + 1, id.length());
result = Long.parseLong(numStr);
}
}
return result;
}
/**
* Does a proper compare on the Id's
*
* @param id1 the lhs of the comparison.
* @param id2 the rhs of the comparison.
* @return 0 if equal else a positive if {@literal id1 > id2} ...
*/
public static int compare(String id1, String id2) {
int result = -1;
String seed1 = IdGenerator.getSeedFromId(id1);
String seed2 = IdGenerator.getSeedFromId(id2);
if (seed1 != null && seed2 != null) {
result = seed1.compareTo(seed2);
if (result == 0) {
long count1 = IdGenerator.getSequenceFromId(id1);
long count2 = IdGenerator.getSequenceFromId(id2);
result = (int) (count1 - count2);
}
}
return result;
}
/**
* When using the {@link java.net.InetAddress#getHostName()} method in an
* environment where neither a proper DNS lookup nor an <tt>/etc/hosts</tt>
* entry exists for a given host, the following exception will be thrown:
* <code>
* java.net.UnknownHostException: &lt;hostname&gt;: &lt;hostname&gt;
* at java.net.InetAddress.getLocalHost(InetAddress.java:1425)
* ...
* </code>
* Instead of just throwing an UnknownHostException and giving up, this
* method grabs a suitable hostname from the exception and prevents the
* exception from being thrown. If a suitable hostname cannot be acquired
* from the exception, only then is the <tt>UnknownHostException</tt> thrown.
*
* @return The hostname
* @throws UnknownHostException if the given host cannot be looked up.
* @see java.net.InetAddress#getLocalHost()
* @see java.net.InetAddress#getHostName()
*/
protected static String getLocalHostName() throws UnknownHostException {
try {
return (InetAddress.getLocalHost()).getHostName();
}
catch (UnknownHostException uhe) {
String host = uhe.getMessage(); // host = "hostname: hostname"
if (host != null) {
int colon = host.indexOf(':');
if (colon > 0) {
return host.substring(0, colon);
}
}
throw uhe;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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() {
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
@ -156,6 +184,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(tcpAmqpConnectionUri), 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 {
@ -173,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 {
@ -894,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
@ -905,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
@ -926,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) {

View File

@ -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>