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