com.crankuptheamps.client.Client Maven / Gradle / Ivy
////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2024 60East Technologies Inc., All Rights Reserved.
//
// This computer software is owned by 60East Technologies Inc. and is
// protected by U.S. copyright laws and other laws and by international
// treaties. This computer software is furnished by 60East Technologies
// Inc. pursuant to a written license agreement and may be used, copied,
// transmitted, and stored only in accordance with the terms of such
// license agreement and with the inclusion of the above copyright notice.
// This computer software or any other copies thereof may not be provided
// or otherwise made available to any other person.
//
// U.S. Government Restricted Rights. This computer software: (a) was
// developed at private expense and is in all respects the proprietary
// information of 60East Technologies Inc.; (b) was not developed with
// government funds; (c) is a trade secret of 60East Technologies Inc.
// for all purposes of the Freedom of Information Act; and (d) is a
// commercial item and thus, pursuant to Section 12.212 of the Federal
// Acquisition Regulations (FAR) and DFAR Supplement Section 227.7202,
// Government's use, duplication or disclosure of the computer software
// is subject to the restrictions set forth by 60East Technologies Inc..
//
////////////////////////////////////////////////////////////////////////////
package com.crankuptheamps.client;
import java.beans.ExceptionListener;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.lang.AutoCloseable;
import java.lang.ClassLoader;
import java.lang.reflect.Method;
import java.lang.Long;
import java.lang.Math;
import java.util.Collection;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import com.crankuptheamps.client.Store.StoreReplayer;
import com.crankuptheamps.client.exception.AMPSException;
import com.crankuptheamps.client.exception.AlreadyConnectedException;
import com.crankuptheamps.client.exception.AuthenticationException;
import com.crankuptheamps.client.exception.BadFilterException;
import com.crankuptheamps.client.exception.BadRegexTopicException;
import com.crankuptheamps.client.exception.BadSowKeyException;
import com.crankuptheamps.client.exception.CommandException;
import com.crankuptheamps.client.exception.ConnectionException;
import com.crankuptheamps.client.exception.ConnectionRefusedException;
import com.crankuptheamps.client.exception.DisconnectedException;
import com.crankuptheamps.client.exception.DuplicateLogonException;
import com.crankuptheamps.client.exception.InvalidBookmarkException;
import com.crankuptheamps.client.exception.InvalidMessageTypeException;
import com.crankuptheamps.client.exception.InvalidOptionsException;
import com.crankuptheamps.client.exception.InvalidOrderByException;
import com.crankuptheamps.client.exception.InvalidSubIdException;
import com.crankuptheamps.client.exception.InvalidTopicException;
import com.crankuptheamps.client.exception.InvalidURIException;
import com.crankuptheamps.client.exception.LogonRequiredException;
import com.crankuptheamps.client.exception.NotEntitledException;
import com.crankuptheamps.client.exception.NameInUseException;
import com.crankuptheamps.client.exception.ProtocolException;
import com.crankuptheamps.client.exception.PublishFilterException;
import com.crankuptheamps.client.exception.RetryOperationException;
import com.crankuptheamps.client.exception.StoreException;
import com.crankuptheamps.client.exception.SubidInUseException;
import com.crankuptheamps.client.exception.SubscriptionAlreadyExistsException;
import com.crankuptheamps.client.exception.TimedOutException;
import com.crankuptheamps.client.exception.TransportTypeException;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.fields.BookmarkField;
import com.crankuptheamps.client.fields.LongField;
import com.crankuptheamps.client.fields.OptionsField;
/**
* The base AMPS client object used for AMPS applications that do not need a
* resilient connection to AMPS. Most production applications use the HAClient
* rather than Client. Each client object manages a single connection to AMPS.
* Each AMPS connection has a name, a specific transport (such as tcp), a
* protocol (used for framing messages to AMPS), and a message type
* (such as FIX or JSON).
*
* The Client object creates and manages background threads. The object
* provides both a synchronous interface for processing messages on the
* calling thread and an asynchronous interface suitable for populating
* a queue to be processed by worker threads.
*
* The Client provides named convenience methods for many AMPS commands.
* These methods provide control over commonly used parameters and support
* most programs. For full control over AMPS, you build a
* {@link com.crankuptheamps.client.Command} object and use
* {@link Client#execute } or {@link Client#executeAsync} to run the command.
*
* AMPS uses the client name to detect duplicate messages, so the name
* of each instance of the client application should be unique.
*
*
* An example of a simple Java program publishing the JSON message
* { "message" : "Hello, World!"}
is listed below.
*
*
*
* public static void main(String[] args) {
* Client client = new Client("ConsolePublisher");
*
* try
* {
* client.connect("tcp://127.0.0.1:9007/amps");
* System.out.println("connected..");
* client.logon();
*
* client.publish("messages", "{\"message\" : \"Hello, World!\"}");
*
* System.exit(0);
* }
* catch (AMPSException e)
* {
* System.err.println(e.getLocalizedMessage());
* e.printStackTrace(System.err);
* }
*
* }
*
*
*
*/
public class Client implements AutoCloseable {
private volatile String name = null;
private volatile String nameHash = null;
private volatile String logonCorrelationData = null;
private Transport transport = null;
// Please do not change this name: there are tests that use reflection and
// depend on it.
private long lastSentSequenceNumber = 0;
private Store publishStore = null;
private volatile boolean connected = false;
private volatile boolean _isRetryOnDisconnect = true;
private volatile int _defaultMaxDepth = Integer.MAX_VALUE;
private volatile URI uri = null;
private volatile ClientDisconnectHandler disconnectHandler = new DefaultDisconnectHandler();
protected volatile ExceptionListener exceptionListener = null;
private Message message = null;
private Command _command = new Command();
private ThreadLocal _publishCommand = new ThreadLocal() {
@Override
protected Command initialValue() {
return new Command(Message.Command.Publish);
}
};
private ThreadLocal _bookmarksData = new ThreadLocal() {
@Override
protected byte[] initialValue() {
return new byte[64];
}
};
private Message beatMessage = null;
// Client lock
protected final Lock lock = new ReentrantLock();
private final Condition _reconnecting = lock.newCondition();
// private final Lock lock = new DebugLock("HAClient");
// Asynchronous->Synchronous ack handling
private final Lock acksLock = new ReentrantLock();
private final Condition ackReceived = acksLock.newCondition();
protected volatile int _lastFailedConnectionVersion = -1;
private final HashSet _connectionStateListeners = new HashSet();
// Queue Acking members
private volatile long _queueAckTimeout = 0;
private volatile boolean _isAutoAckEnabled = false;
private volatile int _ackBatchSize = 0;
private HashBox _hashBox = new HashBox();
private CommandId _queueAckCommandId = CommandId.nextIdentifier();
private static final OptionsField CANCEL_FIELD = new OptionsField(Message.Options.Cancel);
static class QueueBookmarks {
private byte[] _topic;
private byte[] _data;
private long _oldestTime;
private volatile int _bookmarkCount;
private int _length;
}
private Map _topicHashMap = new HashMap();
static class AckResponse {
public volatile int connectionVersion;
public VersionInfo serverVersion;
public volatile boolean responded;
public volatile boolean abandoned;
public volatile int state;
public volatile int reason;
public volatile String reasonText;
public volatile String username;
public volatile String password;
public volatile long sequence;
public volatile String options;
public volatile String bookmark;
}
private Map _acks = new ConcurrentHashMap();
private MessageRouter _routes = new MessageRouter();
private ClientHandler _handler = new ClientHandler(this);
private int version = Version.AMPS_5;
private static volatile String _version = null;
private BookmarkStore bookmarkStore = new DefaultBookmarkStore();
private volatile String username;
private volatile SubscriptionManager subscriptionManager = new DefaultSubscriptionManager();
private volatile boolean _logonInProgress = false;
private volatile boolean _reconnectingSubscriptionManager = false;
private boolean _defaultSubscriptionManager = true;
private volatile FailedWriteHandler _failedWriteHandler = null;
private VersionInfo serverVersion = new VersionInfo("default");
private StopWatch heartbeatTimer = new StopWatch();
private int _heartbeatInterval = 0;
private int _readTimeout = 0;
public static final int MIN_PERSISTED_BOOKMARK_VERSION = 3080000;
public static final int MIN_MULTI_BOOKMARK_VERSION = 4000000;
private static final int MIN_FLUSH_VERSION = 4000000;
private ThreadCreatedHandler _threadCreatedHandler = null;
private TransportFilter _transportFilter = new DefaultTransportFilter();
private ArrayList _httpPreflightHeaders = null;
/**
* Specifies a type of message that is always sent to an associated
* handler when one is set. All messages of this type will be sent to the
* handler in addition to any others that are set based on the command,
* sub, or query id in the message.
*/
public enum GlobalCommandTypeHandlers {
Publish,
SOW,
GroupBegin,
GroupEnd,
Heartbeat,
OOF,
Ack,
LastChance,
DuplicateMessage,
COUNT
}
private final MessageHandler[] _globalCommandTypeHandlers = new MessageHandler[GlobalCommandTypeHandlers.COUNT
.ordinal()];
/**
* Class that holds supported versions of AMPS, those are: AMPS version 3, AMPS
* version 4 and AMPS version 5.
*/
public static final class Version {
// AMPS_1 and AMPS_2 are no longer supported.
// public static final int AMPS_1 = 0x00010000;
// public static final int AMPS_2 = 0x00020000;
public static final int AMPS_3 = 0x00030000;
public static final int AMPS_4 = 0x00040000;
public static final int AMPS_5 = 0x00050000;
}
/**
* Provides constants for special bookmark values. All bookmarks, including
* these special values, are {@link java.lang.String Strings}.
* Functions that take a bookmark such as
* {@link Client#bookmarkSubscribe bookmarkSubscribe} can be passed a
* bookmark ID, a timestamp with the format YYYYmmddTHHMMSS (as
* described in the AMPS User Guide), or one of these special values.
*
*
* For example, to begin a bookmark subscription at the beginning of the
* journal, provide {@code Bookmarks.EPOCH} as the bookmark in the
* {@link Command} object or the call to
* bookmarkSubscribe.
*
*/
public static final class Bookmarks {
/**
* Start the subscription at the first undiscarded message in the
* bookmark store, or at the end of the bookmark store if all
* messages have been discarded.
*/
public static final String MOST_RECENT = "recent";
/**
* Start the subscription at the beginning of the journal.
*/
public static final String EPOCH = "0";
/**
* Start the subscription at the point in time when AMPS processes
* the subscription.
*/
public static final String NOW = "0|1|";
}
/**
* Default constructor that creates a client without a name. This
* constructor exists for use with frameworks that require managed
* objects to have a default constructor. The client MUST still be
* assigned a name using {@link #setName(String)} before connecting.
*
* @see #Client(String)
* @see #setName(String)
*/
public Client() {
this(null);
}
/**
* Creates a client.
*
* @param name Name for the client. This name is used for duplicate
* message detection and should be unique. AMPS does not enforce
* specific restrictions on the character set used, however some
* protocols (for example, XML) may not allow specific characters.
* 60East recommends that the client name be meaningful, short,
* human
* readable, and avoids using control characters, newline
* characters,
* or square brackets.
*/
public Client(String name) {
this(name, null);
}
/**
* Creates a client.
*
* @param name Name for the client. This name is used for duplicate
* message detection and should be unique.AMPS does not enforce
* specific restrictions on the character set used, however some
* protocols (for example, XML) may not allow specific
* characters.
* 60East recommends that the client name be meaningful, short,
* human
* readable, and avoids using control characters, newline
* characters,
* or square brackets.
* @param version Server version connecting to
*/
public Client(String name, int version) {
this(name, null, version);
}
/**
* Creates a client with a transport.
*
* @param name Name for the client. This name is used for duplicate
* message detection and should be unique.
* @param transport Transport to use for this client
*/
public Client(String name, Transport transport) {
this(name, transport, Version.AMPS_5);
}
/**
* Creates a client with a transport
*
* @param name Name for the client. This name is used for duplicate
* message detection and should be unique.
* @param transport Transport to use with the client
* @param version Server version connecting to. Only AMPS_3 and greater are
* supported.
*/
public Client(String name, Transport transport, int version) {
this.name = name;
if (transport != null) {
this.transport = transport;
this.transport.setMessageHandler(getTransportMessageHandler());
this.transport.setDisconnectHandler(getTransportDisconnectHandler());
this.transport.setIdleRunnable(new AckFlusherRunnable());
this.transport.initFromClient(this);
this.message = this.transport.allocateMessage();
this.message._client = this;
this.beatMessage = this.transport.allocateMessage();
this.beatMessage.setCommand(Message.Command.Heartbeat);
this.beatMessage.setOptions("beat");
}
this.version = version;
this.replayer = new ClientStoreReplayer(this);
for (int i = 0; i < GlobalCommandTypeHandlers.COUNT.ordinal(); ++i) {
_globalCommandTypeHandlers[i] = new DefaultMessageHandler();
}
}
/**
* This method MUST be called to assign a unique client name before
* connecting to an AMPS server when the client was constructed using
* the default constructor. This is a write-once property. If the
* client already has a non-empty client name, this method will throw
* {@link IllegalStateException}. This method and the default constructor
* exist to better support frameworks that require managed objects to
* have a default constructor.
*
* @param name The client name, passed to the server to uniquely identify
* this client across sessions. AMPS does not enforce
* specific restrictions on the character set used, however some
* protocols (for example, XML) may not allow specific characters.
* 60East recommends that the client name be meaningful, short, human
* readable, and avoids using control characters, newline characters,
* or square brackets.
*
* @throws IllegalStateException If this is called when the client
* already has a non-empty name.
*
* @see #Client()
* @see #Client(String)
*/
public void setName(String name) {
if (this.name != null && !this.name.isEmpty()) {
throw new IllegalStateException("Client name is a write-once "
+ "property, and it already has a non-empty value: " + this.name);
}
this.name = name;
}
/**
* Return the name of the Client.
*
* @return String Client name
*/
public String getName() {
return this.name;
}
/**
* Return the name hash of the Client as returned by the server at logon.
*
* @return String Client name
*/
public String getNameHash() {
return this.nameHash;
}
/**
* Return the URI the Client is connected to.
*
* @return URI
*/
public URI getURI() {
return this.uri;
}
/**
* Return the server version retrieved during logon. If the client has
* not logged on or is connected to a server whose version is less than
* 3.8.0.0 this function will return 0.
* The version uses 2 digits each for major minor maintenance and hotfix
* i.e., 3.8.1.5 will return 3080105
* Versions of AMPS prior to 3.8.0.0 did not return the server version
* to the client in the logon ack.
* Version parts with more than 2 digits will be set to 99. This function will
* be deprecated in a future version of the client.
*
* @return The version of the server as a long.
*/
public int getServerVersion() {
return this.serverVersion.getOldStyleVersion();
}
/**
* Return the server version retrieved during logon. If the client has
* not logged on or is connected to a server whose version is less than
* 3.8.0.0 this function will return a version "default".
* Versions of AMPS prior to 3.8.0.0 did not return the server version
* to the client in the logon ack.
*
* @return The version of the server as a VersionInfo.
*/
public VersionInfo getServerVersionInfo() {
return this.serverVersion;
}
/**
* WARNING: @deprecated Use {@link VersionInfo#getVersion()}
* Return the numeric value for the given version string with the pattern:
* Major.Minor.Maintenance.Hotfix
* The version uses 2 digits each for major minor maintenance and hotfix
* i.e., 3.8.1.5 will return 3080105
* Version strings passed in can be shortened to not include all levels
* so 3.8 will return 3080000.
* Version parts with more than 2 digits will be set to 99. This function will
* be deprecated in a future version of the client.
*
* @param version The version string to convert.
* @return The version as a int.
* @throws CommandException The string doesn't represent a valid version.
*/
@Deprecated
static public int getVersionAsInt(String version) throws CommandException {
if (version == null || version.length() == 0)
return 0;
int retVersion = 0;
int c = 0;
int dots = 0;
int lastDot = -1;
byte[] bytes = version.getBytes(StandardCharsets.ISO_8859_1);
for (int i = 0; dots < 4 && i < version.length(); ++i) {
c = bytes[i];
if (c == '.') {
++dots;
retVersion *= 10;
if (i - lastDot > 5) {
throw new CommandException("Too many digits between dots found translating version string.");
} else if (i - lastDot > 3) {
retVersion *= 10;
retVersion += 99;
} else {
if (i - lastDot == 3) {
retVersion += bytes[i - 2] - '0';
}
retVersion *= 10;
retVersion += bytes[i - 1] - '0';
}
lastDot = i;
} else if (c < '0' || c > '9') {
throw new CommandException("Invalid character found in version string.");
} else if (i == version.length() - 1) {
++dots;
retVersion *= 10;
if (i - lastDot > 4) {
throw new CommandException("Too many digits between dots found translating version string.");
} else if (i - lastDot > 2) {
retVersion *= 10;
retVersion += 99;
} else {
if (i - lastDot == 2) {
retVersion += bytes[i - 1] - '0';
}
retVersion *= 10;
retVersion += bytes[i] - '0';
}
}
}
for (; dots < 4; ++dots)
retVersion *= 100;
return retVersion;
}
/**
* Return the underlying transport.
* For advanced users, this method provides direct access to the transport.
* Care needs to be taken to not modify the underlying transport in ways that
* are incompatible with the Client.
*
* @return Transport Underlying transport instance
*/
public Transport getTransport() {
try {
lock.lock();
return this.transport;
} finally {
lock.unlock();
}
}
/**
* Sets the underlying bookmark store, which is used to track which
* messages the client has received and which messages have been
* processed by the program. This method replaces any existing
* bookmark store for the client, without transferring the state
* of that store.
*
* @param val The new bookmark store.
* @throws AlreadyConnectedException The store cannot be set while the client is
* connected.
*/
public void setBookmarkStore(BookmarkStore val) throws AlreadyConnectedException {
try {
lock.lock();
if (this.connected) {
throw new AlreadyConnectedException(
"Setting a bookmark store on a connected client is undefined behavior.");
}
if (val != null) {
this.bookmarkStore = val;
} else {
this.bookmarkStore = new DefaultBookmarkStore();
}
} finally {
lock.unlock();
}
}
/**
* Returns the underlying bookmark store for this client. The bookmark store
* is used to track which messages the client has received and which
* messages have been processed by the program.
*
* @return BookmarkStore The bookmark store for this client.
*/
public BookmarkStore getBookmarkStore() {
return this.bookmarkStore;
}
/**
* Sets the underlying publish store, which is used to store
* published messages until the AMPS instance has acknowledged
* those messages. This method replaces any existing
* publish store for the client, without transferring the state
* of that store.
*
* @param store The new publish store.
* @throws AlreadyConnectedException The store cannot be set while the client is
* connected.
*/
public void setPublishStore(Store store) throws AlreadyConnectedException {
try {
lock.lock();
if (this.connected) {
throw new AlreadyConnectedException(
"Setting a publish store on a connected client is undefined behavior.");
}
this.publishStore = store;
} finally {
lock.unlock();
}
}
/**
* Returns the underlying publish store for this client. The publish store
* is used to store published messages until the AMPS instance has
* acknowledged those messages.
*
* @return Store The publish store for this client.
*/
public Store getPublishStore() {
return this.publishStore;
}
/**
* Connects to the AMPS instance through the provided URI.
*
* The URI is a {@link String} with the format:
* transport://user:password@host:port/protocol
*
*
*
* - transport -- the network transport used for the connection
* - user -- the userId used to authenticate the connection
* - password -- the password used to authenticate the connection
* - host -- the hostname or IP address of the host where AMPS is
* running
* - port -- the port to connect to
*
- protocol -- the protocol used for the connection
*
*
*
* Notice that the protocol can be independent of the message type.
* 60East recommends using the amps
protocol, although some
* installations use one of the legacy protocols such as fix
,
* nvfix
or xml
. Contact your server
* administrator for the correct URI for the instance.
*
*
*
* Authentication is optional if the system is using the default
* authentication provided with AMPS. This default authentication allows
* all users to authenticate, regardless of the user or password provided.
* If your AMPS installation uses custom authentication, use the
* Authenticator appropriate for that authentication mechanism.
*
*
* @param uri The URI string to connect to
* @throws ConnectionRefusedException The connection could not be established
* @throws AlreadyConnectedException The connection is already connected
* @throws InvalidURIException The specified URI is invalid
* @throws ProtocolException The protocol is invalid
* @throws TransportTypeException The transport type is invalid
*/
public void connect(String uri) throws ConnectionException {
lock.lock();
try {
if (this.name == null || this.name.isEmpty()) {
throw new ConnectionException("Client name must be set to a "
+ "non-empty value before connecting.");
}
URI connectURI = null;
try {
connectURI = new URI(uri);
} catch (URISyntaxException urisex) {
throw new InvalidURIException(urisex);
}
String uriPath = connectURI.getPath();
String[] sPath = uriPath.substring(1, uriPath.length()).split("/");
String sProtocol = sPath[sPath.length-1];
// Only amps protocol supports a path of more than 1
if (sPath.length > 1) {
// If final item was message type, protocol should precede it
if (!"amps".equals(sProtocol)) {
sProtocol = sPath[sPath.length-2];
}
if (!"amps".equals(sProtocol)) {
throw new InvalidURIException("Specification of message type requires amps protocol");
}
}
String scheme = connectURI.getScheme();
if (this.transport == null) {
this.uri = connectURI;
Properties props = new URIProperties(this.uri);
Protocol messageType = ProtocolFactory.createProtocol(sProtocol, props);
this.transport = TransportFactory
.createTransport(scheme, messageType, props);
this.message = this.transport.allocateMessage();
this.message._client = this;
this.beatMessage = this.transport.allocateMessage();
this.beatMessage.setCommand(Message.Command.Heartbeat);
this.beatMessage.setOptions("beat");
if (publishStore != null)
publishStore.setMessage(transport.allocateMessage());
this.transport.setMessageHandler(getTransportMessageHandler());
this.transport.setDisconnectHandler(getTransportDisconnectHandler());
this.transport.setIdleRunnable(new AckFlusherRunnable());
this.transport.initFromClient(this);
}
else if (this.uri == null) {
this.uri = connectURI;
}
else {
String oldScheme = this.uri.getScheme();
String oldPath = this.uri.getPath();
String[] sOldPath = oldPath.substring(1, oldPath.length()).split("/");
String sOldProtocol = sOldPath[sOldPath.length-1];
// Only amps protocol supports a path of more than 1
if (sOldPath.length > 1 && !"amps".equals(sOldProtocol)) {
sOldProtocol = sOldPath[sOldPath.length-2];
}
if (!oldScheme.equals(scheme)) {
throw new ConnectionException("Client cannot change transport type from " + oldScheme + " to " + scheme);
}
if (!sOldProtocol.equals(sProtocol)) {
throw new ConnectionException("Client cannot change protocol type from " + sOldProtocol + " to " + sProtocol);
}
this.uri = connectURI;
}
this.transport.connect(this.uri);
broadcastConnectionStateChanged(ConnectionStateListener.Connected);
this.connected = true;
} finally {
lock.unlock();
}
}
/**
* Sets the {@link ClientDisconnectHandler}. In the event that the Client is
* unintentionally disconnected from AMPS, the invoke
method
* from the ClientDisconnectHandler will be invoked.
*
* @param disconnectHandler_ The disconnect handler
*/
public void setDisconnectHandler(ClientDisconnectHandler disconnectHandler_) {
if (disconnectHandler_ != null) {
this.disconnectHandler = disconnectHandler_;
} else {
this.disconnectHandler = new DefaultDisconnectHandler();
}
}
/**
* Returns the current {@link ClientDisconnectHandler} set on self.
*
* @return The current disconnect handler.
*/
public ClientDisconnectHandler getDisconnectHandler() {
return this.disconnectHandler;
}
/**
*
* Sets the {@link MessageHandler} instance used when no other handler matches.
*
* @param messageHandler The message handler used when no other handler matches.
*
* @deprecated Use {@link #setLastChanceMessageHandler } instead.
*/
@Deprecated
public void setUnhandledMessageHandler(MessageHandler messageHandler) {
setLastChanceMessageHandler(messageHandler);
}
/**
* Sets the {@link MessageHandler} instance used when no other handler matches.
*
* @param messageHandler The message handler used when no other handler matches.
*/
public void setLastChanceMessageHandler(MessageHandler messageHandler) {
if (messageHandler != null) {
this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()] = messageHandler;
} else {
this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance
.ordinal()] = new DefaultMessageHandler();
}
}
/**
* Sets the {@link java.beans.ExceptionListener} instance used for communicating
* absorbed exceptions.
*
* @param exceptionListener The exception listener instance to invoke for
* exceptions.
*/
public void setExceptionListener(ExceptionListener exceptionListener) {
this.exceptionListener = exceptionListener;
if (this.transport != null) {
this.transport.setExceptionListener(exceptionListener);
}
}
/**
* Returns the {@link java.beans.ExceptionListener} instance used for
* communicating absorbed exceptions.
*
* @return The current exception listener.
*/
public ExceptionListener getExceptionListener() {
return this.exceptionListener;
}
/**
* Sets the {@link SubscriptionManager} instance used for recording active
* subscriptions.
*
* @param subscriptionManager The subscription manager invoked when the
* subscriptions change.
*/
public void setSubscriptionManager(SubscriptionManager subscriptionManager) {
if (subscriptionManager != null) {
this.subscriptionManager = subscriptionManager;
_defaultSubscriptionManager = subscriptionManager instanceof DefaultSubscriptionManager;
} else {
this.subscriptionManager = new DefaultSubscriptionManager();
_defaultSubscriptionManager = true;
}
}
/**
* Returns the {@link SubscriptionManager} instance used for recording active
* subscriptions.
*
* @return The current subscription manager.
*/
public SubscriptionManager getSubscriptionManager() {
return this.subscriptionManager;
}
void absorbedException(Exception e) {
try {
if (this.exceptionListener != null) {
this.exceptionListener.exceptionThrown(e);
}
} catch (Exception e2) {
; // Absorb exception from the exception listener
}
}
/**
* Sets the {@link MessageHandler} instance used for all messages of the
* given command type.
*
* @param command The type of command to send to this handler
* @param messageHandler The message handler to invoke for duplicate messages
* @throws CommandException If the command is not a supported command.
*/
public void setGlobalCommandTypeMessageHandler(String command,
MessageHandler messageHandler)
throws CommandException {
MessageHandler handler = messageHandler;
if (handler == null) {
handler = new DefaultMessageHandler();
}
switch (command.charAt(0)) {
/*
* These types are not currently allowed to avoid extra branching
* in message delivery.
* case 'p':
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.Publish,
* handler);
* break;
* case 's':
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.SOW,
* handler);
* break;
* case 'g':
* if (command.charAt(6) == 'b') {
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.GroupBegin,
* handler);
* }
* else if (command.charAt(6) == 'e') {
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.GroupEnd,
* handler);
* }
* else {
* throw new CommandException("Invalid command " + command
* + " passed to setGlobalCommandTypeMessageHandler");
* }
* break;
* case 'o':
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.OOF,
* handler);
* break;
*/
case 'h':
setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers.Heartbeat,
handler);
break;
case 'a':
setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers.Ack,
handler);
break;
case 'l':
case 'L':
setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers.LastChance,
handler);
break;
case 'd':
case 'D':
setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers.DuplicateMessage,
handler);
break;
default:
throw new CommandException("Invalid command " + command
+ " passed to setGlobalCommandTypeMessageHandler");
}
}
/**
* Sets the {@link MessageHandler} instance used for all messages of the
* given command type.
*
* @param command The Message.Command value of the desired command to
* send to this handler.
* @param messageHandler The message handler to invoke for duplicate messages
* @throws CommandException If the command is not a supported command.
*/
public void setGlobalCommandTypeMessageHandler(int command,
MessageHandler messageHandler)
throws CommandException {
MessageHandler handler = messageHandler;
if (handler == null) {
handler = new DefaultMessageHandler();
}
switch (command) {
/*
* These types are not currently allowed to avoid extra branching
* in message delivery.
* case Message.Command.Publish:
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.Publish,
* handler);
* break;
* case Message.Command.SOW:
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.SOW,
* handler);
* break;
* case Message.Command.GroupBegin:
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.GroupBegin,
* handler);
* break;
* case Message.Command.GroupEnd:
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.GroupEnd,
* handler);
* break;
* case Message.Command.OOF:
* setGlobalCommandTypeMessageHandler(
* GlobalCommandTypeHandlers.OOF,
* handler);
* break;
*/
case Message.Command.Heartbeat:
setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers.Heartbeat,
handler);
break;
case Message.Command.Ack:
setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers.Ack,
handler);
break;
default:
throw new CommandException("Invalid command " + command
+ " passed to setGlobalCommandTypeMessageHandler");
}
}
/**
* Sets the {@link MessageHandler} instance used for all messages of the
* given command type.
*
* @param command The GlobalCommandTypeHandlers value of the desired
* command to send to this handler.
* @param messageHandler The message handler to invoke for duplicate messages
* @throws CommandException If the command is not a supported command.
*/
public void setGlobalCommandTypeMessageHandler(
GlobalCommandTypeHandlers command,
MessageHandler messageHandler)
throws CommandException {
MessageHandler handler = messageHandler;
if (handler == null) {
handler = new DefaultMessageHandler();
}
_globalCommandTypeHandlers[command.ordinal()] = handler;
}
/**
* Sets the {@link MessageHandler} instance used for duplicate messages.
*
* @param messageHandler The message handler to invoke for duplicate messages
*/
public void setDuplicateMessageHandler(MessageHandler messageHandler) {
MessageHandler handler = messageHandler;
if (handler == null) {
handler = new DefaultMessageHandler();
}
this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.DuplicateMessage.ordinal()] = handler;
}
/**
* Returns the {@link MessageHandler} instance used for duplicate messages.
*
* @return The message handler to invoke for duplicate messages
*/
public MessageHandler getDuplicateMessageHandler() {
return this._globalCommandTypeHandlers[GlobalCommandTypeHandlers.DuplicateMessage.ordinal()];
}
/**
* Sets the {@link FailedWriteHandler} instance used to report on failed
* messages that have been written.
*
* @param handler_ The handler to invoke for published duplicates.
*/
public void setFailedWriteHandler(FailedWriteHandlerV4 handler_) {
this._failedWriteHandler = new FailedWriteHandlerV4Compat(handler_);
}
/**
* Sets the {@link FailedWriteHandler} instance used to report on failed
* messages that have been written.
*
* @param handler_ The handler to invoke for published duplicates.
*/
public void setFailedWriteHandler(FailedWriteHandler handler_) {
this._failedWriteHandler = handler_;
}
/**
* Returns the {@link FailedWriteHandler} instance used to report on failed
* messages that have been written.
*
* @return The handler to invoke for published duplicates.
*/
public FailedWriteHandler getFailedWriteHandler() {
return this._failedWriteHandler;
}
/**
* Adds a {@link ConnectionStateListener} instance that will be invoked when
* this client connects or disconnects.
*
* @param listener_ The instance to invoke.
* @since 4.0.0.0
*/
public void addConnectionStateListener(ConnectionStateListener listener_) {
lock.lock();
try {
this._connectionStateListeners.add(listener_);
} finally {
lock.unlock();
}
}
/**
* Removes a {@link ConnectionStateListener} from being invoked when this client
* connects or disconnects.
*
* @param listener_ The instance to remove.
* @since 4.0.0.0
*/
public void removeConnectionStateListener(ConnectionStateListener listener_) {
lock.lock();
try {
this._connectionStateListeners.remove(listener_);
} finally {
lock.unlock();
}
}
/**
* This method is responsible for broadcasting changes in the connection state to all registered
* ConnectionStateListener objects. It is used to notify listeners when the connection state changes.
*
* The method obtains a copy of the listeners under lock to ensure thread safety and then broadcasts
* the connection state change to each listener. This approach allows listeners to remove themselves
* from the list during the iteration.
*
* @param newState_ An integer representing the new connection state. This value is provided to
* listeners through the connectionStateChanged method of the ConnectionStateListener
* interface.
*/
protected void broadcastConnectionStateChanged(int newState_) {
// Get a copy of the listeners under lock, then broadcast.
// Allows listeners to remove themselves.
ConnectionStateListener[] listeners = null;
lock.lock();
try {
listeners = new ConnectionStateListener[_connectionStateListeners.size()];
_connectionStateListeners.toArray(listeners);
} finally {
lock.unlock();
}
for (ConnectionStateListener listener : listeners) {
try {
listener.connectionStateChanged(newState_);
} catch (Exception ex_) {
absorbedException(ex_);
}
}
}
/**
* Disconnect from the AMPS server.
*
*/
public void disconnect() {
_disconnect(false);
}
/**
* Disconnect from the AMPS server.
*
*/
void _disconnect(boolean reconnecting) {
Transport currentTransport = null;
try {
if (!reconnecting && getAckBatchSize() > 1)
flushAcks();
} catch (AMPSException e) {
absorbedException(e);
}
lock.lock();
try {
currentTransport = this.transport;
heartbeatTimer.setTimeout(0);
if (this.connected)
broadcastConnectionStateChanged(ConnectionStateListener.Disconnected);
this.connected = false;
} finally {
lock.unlock();
}
// don't disconnect the transport with the client lock held.
if (currentTransport != null)
currentTransport.disconnect();
lock.lock();
try {
// only disconnect if a transport has been created
_routes.clear();
// Signal and then clear the ack info
cancelSynchronousWaiters(Integer.MAX_VALUE);
// Only broadcast Shutdown if we're not reconnecting.
if (!reconnecting) {
broadcastConnectionStateChanged(ConnectionStateListener.Shutdown);
}
} catch (Exception e) {
absorbedException(e);
} finally {
lock.unlock();
}
}
/**
* Disconnect from the AMPS server.
*
*/
public void close() {
disconnect();
}
/**
* Creates a new {@link Message} appropriate for this client. This function
* should be
* called rarely, since it does allocate a handful of small objects. Users
* sensitive
* to garbage collection delays should cache the message object for later usage.
*
* @return A new {@link Message} instance
* @throws DisconnectedException When the client has been closed or has never
* been connected.
*/
public Message allocateMessage() throws DisconnectedException {
if (this.transport == null) {
throw new DisconnectedException("Cannot allocate a message "
+ " while not connected.");
}
try {
lock.lock();
return this.transport.allocateMessage();
} finally {
lock.unlock();
}
}
/**
* Send a {@link Message} to AMPS via the {@link Transport} used in the Client.
* This method is provided for special cases. In general, you can get the
* same results with additional error checking by using a {@link Command}
* object with the execute or executeAsync methods.
*
* @param message The message to send
* @throws DisconnectedException The connection was disconnected at time of send
*/
public void send(Message message) throws DisconnectedException {
lock.lock();
try {
sendInternal(message);
} finally {
lock.unlock();
}
}
/**
* Adds a MessageHandler for a given CommandId to self.
*
* @param commandId_ The command, query, or subid used to invoke the
* handler.
* @param messageHandler_ The message handler to route to
* @param requestedAcks_ The acks requested to be sent to the handler
* @param isSubscribe_ True if this route is for an ongoing subscription
*/
public void addMessageHandler(CommandId commandId_,
MessageHandler messageHandler_,
int requestedAcks_,
boolean isSubscribe_) {
_routes.addRoute(commandId_, messageHandler_, requestedAcks_, 0, isSubscribe_);
}
/**
* Remove a MessageHandler for a given CommandId from self.
*
* @param commandId_ The commandId for the handler to remove
* @return true if the handler was removed.
*/
public boolean removeMessageHandler(CommandId commandId_) {
return _routes.removeRoute(commandId_);
}
private int sendWithoutRetry(Message message) throws DisconnectedException {
if (this.transport != null) {
if (_logonInProgress && message.getCommand() != Message.Command.Logon) {
throw new DisconnectedException("The client has been disconnected");
}
int version = transport.getVersion();
transport.sendWithoutRetry(message);
return version;
} else
throw new DisconnectedException("There is no current connection");
}
private int sendInternal(Message message) throws DisconnectedException {
return sendInternal(message, 0, false);
}
private int sendInternal(Message message, long haSeq)
throws DisconnectedException {
return sendInternal(message, haSeq, false);
}
/**
* Only messages stored in a publish store will set haSeq to non-zero
* Only messages stored in a subscription manager will set isHASubscribe.
*/
private int sendInternal(Message message, long haSeq, boolean isSubscribe)
throws DisconnectedException {
assert ((ReentrantLock) lock).isHeldByCurrentThread();
while (true) {
// It's not safe to HA publish while a reconnect is in progress.
// When a reconnect is in progress, we might be able to sneak
// a publish through, but if it's sequence number is higher
// than the beginning of what we need to replay, the server will
// take that message to mean that we're done replaying, and ignore
// what we replay. If, once we have the lock, we discover
// it's a bad time to HA publish, we'll spin waiting for the
// reconnect to finish up, and then go.
if (haSeq != 0 && _logonInProgress) {
// If retry is disabled, give up
if (!_isRetryOnDisconnect)
throw new DisconnectedException();
// An interrupted exception is ignored to recheck
try {
_reconnecting.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
} else {
Transport currentTransport = this.transport;
if (currentTransport == null) {
throw new DisconnectedException("There is no current connection.");
}
int version = currentTransport.getVersion();
if ((haSeq != 0 && (haSeq <= lastSentSequenceNumber
|| lastSentSequenceNumber == 0))
|| (isSubscribe && _reconnectingSubscriptionManager))
return -1;
// It's possible to get here out of sequence number order,
// try sending lower sequence number messages first.
while (haSeq > (lastSentSequenceNumber + 1)) {
try {
// This increments lastSentSequenceNumber
if (!publishStore.replaySingle(replayer,
lastSentSequenceNumber + 1)) {
// An interrupted exception is ignored to recheck
try {
_reconnecting.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
} catch (AMPSException ex) {
return version;
}
}
try {
boolean isLogonCmd = message.getCommand() == Message.Command.Logon;
while (_logonInProgress && !isLogonCmd) {
// An interrupted exception is ignored to recheck
try {
_reconnecting.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
currentTransport.sendWithoutRetry(message);
if (haSeq != 0) {
++lastSentSequenceNumber;
}
return version;
} catch (DisconnectedException d) {
if (_isRetryOnDisconnect && message.getCommand() != Message.Command.Logon) {
// attempt to reconnect. If we get anything
// other than a Retry(meaning it succeeded),
// let the exception flow upwards.
lock.unlock();
try {
currentTransport.handleCloseEvent(version, "Error occurred while sending message", d);
} catch (RetryOperationException r) {
if (isSubscribe || haSeq != 0)
return version;
} finally {
lock.lock();
}
} else {
throw d;
}
}
}
}
}
private boolean _registerHandler(CommandId qid, CommandId subid,
CommandId cid,
MessageHandler messageHandler,
int requestedAcks, int systemAddedAcks,
boolean isSubscribe) {
boolean added = (qid != null || subid != null || cid != null);
if (qid != null && _routes.findRoute(qid) == null) {
_routes.addRoute(qid, messageHandler, requestedAcks,
systemAddedAcks, isSubscribe);
added = true;
}
if (subid != null && _routes.findRoute(subid) == null) {
if (qid == null || !subid.equals(qid)) {
_routes.addRoute(subid, messageHandler, requestedAcks,
systemAddedAcks, isSubscribe);
}
if (requestedAcks != Message.AckType.None && cid != subid) {
_routes.addRoute(cid, messageHandler, requestedAcks,
systemAddedAcks, isSubscribe);
}
added = true;
} else if (cid != null && (qid == null || !cid.equals(qid)) &&
_routes.findRoute(cid) == null) {
_routes.addRoute(cid, messageHandler, requestedAcks,
systemAddedAcks, isSubscribe);
added = true;
}
return added;
}
/**
*
* Set whether or not messages being sent to the server should retry if the
* client is disconnected. This is most useful if you are publishing data that
* has a very short lifetime and so is no longer relevant after the time it
* takes to reconnect.
*
* @param isRetryOnDisconnect False to disable default behavior of automatic
* retry.
*
**/
public void setRetryOnDisconnect(boolean isRetryOnDisconnect) {
_isRetryOnDisconnect = isRetryOnDisconnect;
}
/**
*
* Return whether or not messages being sent to the server will retry if the
* client is disconnected.
*
* @return true if it will retry, false if not.
*
**/
public boolean getRetryOnDisconnect() {
return _isRetryOnDisconnect;
}
/**
*
* Sets the default value used for the max depth of {@link MessageStream}
* instances created by this client. This value defaults to
* {@link Integer#MAX_VALUE}. Note: changing this value will not affect
* any message stream instances that have already been created by this
* client; instead use {@link MessageStream#maxDepth(int)}.
*
*
* WARNING: Before setting this, be sure to read important limitations
* and restrictions of this feature discussed here:
* {@link MessageStream#maxDepth(int)}
*
*
* @param md The default max depth of all message stream instances created
* by this client instance.
*
* @see MessageStream#maxDepth(int)
*/
public void setDefaultMaxDepth(int md) {
_defaultMaxDepth = md;
}
/**
* Gets the currently set default value for the max depth of
* {@link MessageStream} instances created by this client.
*
* @see #setDefaultMaxDepth(int)
* @return The current default max depth.
*/
public int getDefaultMaxDepth() {
return _defaultMaxDepth;
}
/**
* Send a {@link Message} to AMPS and register the messageHandler for any
* messages resulting from the command execution.
*
* This method is provided for special cases. In general, you can get the
* same results with additional error checking by using a {@link Command}
* object with the execute or executeAsync methods.
*
* @param messageHandler The message handler that'll receive messages for this
* command
* @param message The message to send
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command, or null if none is
* assigned.
* @throws AMPSException An exception occurred while sending or waiting for a
* response to this Message.
*/
@SuppressWarnings("fallthrough")
public CommandId send(MessageHandler messageHandler, Message message, long timeout)
throws AMPSException {
CommandId id = null;
String strId = message.getCommandId();
if (strId != null) {
id = new CommandId(strId);
}
int requestedAcks = message.getAckTypeOutgoing();
int systemAddedAcks = 0;
boolean isSubscribe = false;
switch (message.getCommand()) {
// Subscriptions need to have a sub id
case Message.Command.Subscribe:
case Message.Command.DeltaSubscribe:
if (this.bookmarkStore != null &&
message.getBookmark() != null) {
systemAddedAcks |= Message.AckType.Persisted;
}
// fall through
case Message.Command.SOWAndSubscribe:
case Message.Command.SOWAndDeltaSubscribe:
if (id == null) {
id = CommandId.nextIdentifier();
message.setCommandId(id);
}
if (message.getSubId() == null) {
message.setSubId(id);
}
isSubscribe = true;
// fall through.
case Message.Command.SOW:
// SOW queries just need a QueryId.
if (id == null) {
id = CommandId.nextIdentifier();
message.setCommandId(id);
}
if (message.getQueryId() == null) {
message.setQueryId(id);
}
systemAddedAcks |= Message.AckType.Processed;
// for SOW only, we get a completed ack so we know when to remove the handler.
if (!isSubscribe)
systemAddedAcks |= Message.AckType.Completed;
message.setAckType(requestedAcks | systemAddedAcks);
if (messageHandler != null) {
CommandId subCmdId = new CommandId(message.getSubId());
CommandId qid = new CommandId(message.getQueryId());
// This will never return false, we set ids in this function.
_registerHandler(qid, subCmdId, id, messageHandler,
requestedAcks, systemAddedAcks,
isSubscribe);
}
lock.lock();
try {
// We aren't adding to subscription manager, so this isn't
// an HA subscribe.
this.syncAckProcessing(id, message, timeout, 0, false);
} catch (TimedOutException e) {
_routes.removeRoute(id);
throw e;
} finally {
lock.unlock();
}
break;
// These are valid commands that are used as-is
case Message.Command.Unsubscribe:
case Message.Command.Heartbeat:
case Message.Command.Logon:
case Message.Command.StartTimer:
case Message.Command.StopTimer:
case Message.Command.DeltaPublish:
case Message.Command.Publish:
case Message.Command.SOWDelete:
// if an ack is requested, it'll need a command ID.
if (requestedAcks != Message.AckType.None) {
if (id == null) {
id = CommandId.nextIdentifier();
message.setCommandId(id);
}
if (messageHandler != null) {
_routes.addRoute(id, messageHandler, requestedAcks, 0, false);
}
}
this.send(message);
break;
// These are things that shouldn't be sent (not meaningful)
case Message.Command.GroupBegin:
case Message.Command.GroupEnd:
case Message.Command.OOF:
case Message.Command.Ack:
case Message.Command.Unknown:
default:
throw new CommandException("Command type can not be sent directly to AMPS");
}
return id;
}
/**
* Execute the provided command and return messages received in response
* in a {@link MessageStream}.
*
* This method creates a message based on the provided {@link Command},
* sends the message to AMPS, and receives the results. AMPS sends the
* message and receives the results on a background thread. That thread
* populates the MessageStream returned by this method.
*
* @param command The Command object containing the command to send to AMPS
* @return A MessageStream that provides messages received in response to the
* command
* @since 4.0.0.0
* @throws AMPSException Thrown when the command fails.
*/
public MessageStream execute(Command command) throws AMPSException {
// If the command is sow and has a sub_id, OR
// if the command has a replace option, return the existing
// messagestream, don't create a new one.
CommandId cid = null;
CommandId subIdCmdId = command.getSubId();
int commandEnum = command.getCommand();
int ackType = command.getAckType();
String options = command.getOptions();
boolean useExistingHandler = subIdCmdId != null
&& ((options != null && options.contains("replace")) || commandEnum == Message.Command.SOW);
if (useExistingHandler) {
// Try to find the exist message handler.
byte[] subId = subIdCmdId.id;
if (subId != null) {
MessageHandler existingHandler = _routes.findRoute(subIdCmdId);
if (existingHandler != null) {
// we found an existing handler. It might not be a message stream, but that's
// okay.
executeAsync(command, existingHandler);
// This will return null if handler was not a messagestream.
return existingHandler instanceof MessageStream ? (MessageStream) existingHandler : null;
}
}
// fall through; we'll use a new handler altogether.
} else if ((commandEnum & Message.Command.NoDataCommands) != 0
&& (ackType == Message.AckType.Persisted ||
ackType == Message.AckType.None)) {
// For publish/delta_publish with no acks, empty message stream
executeAsync(command, null);
return MessageStream.getEmptyMessageStream();
}
MessageStream result = new MessageStream(this);
// Set the default value for max depth on the message stream.
result.maxDepth(_defaultMaxDepth);
try {
cid = executeAsync(command, result);
} catch (AMPSException e) {
// If any exception causes executeAsync() to exit abnormally,
// be sure to close resources associated with the message stream.
result.close();
throw e;
}
boolean statsAck = (commandEnum != Message.Command.Subscribe
&& commandEnum != Message.Command.DeltaSubscribe
&& commandEnum != Message.Command.SOWAndSubscribe
&& commandEnum != Message.Command.SOWAndDeltaSubscribe
&& (command._message.getAckTypeOutgoing() & Message.AckType.Stats) != 0);
if (statsAck) {
result.setStatsOnly();
result.setQueryId(command.getQueryId());
} else if (commandEnum == Message.Command.SOW) {
result.setSOWOnly();
result.setQueryId(command.getQueryId());
} else if (cid != null) {
if (command.isSubscribe()) {
result.setSubscription(cid);
if (!cid.equals(command.getCommandId())) {
result.setCommandId(command.getCommandId());
}
if (commandEnum == Message.Command.SOWAndSubscribe ||
commandEnum == Message.Command.SOWAndDeltaSubscribe) {
result.setQueryId(command.getQueryId());
}
} else if (commandEnum == Message.Command.Publish ||
commandEnum == Message.Command.DeltaPublish ||
commandEnum == Message.Command.SOWDelete) {
result.setAcksOnly(cid, command._message.getAckTypeOutgoing() &
~Message.AckType.Persisted);
} else {
result.setAcksOnly(cid, command._message.getAckTypeOutgoing());
}
} else {
result.close();
result = null;
}
return result;
}
/**
* Execute the provided command and process responses on
* the client receive thread using the provided handler.
*
* This method creates a message based on the provided {@link Command},
* sends the message to AMPS, and invokes the provided
* {@link MessageHandler } to process messages returned in response
* to the command. Rather than providing messages on the calling thread,
* AMPS runs the MessageHandler directly on the client receive thread.
*
* When the provided MessageHandler is not null, this method blocks
* until AMPS acknowledges that the command has been processed. The results
* of the command after that acknowledgement are provided to the
* MessageHandler.
*
* @param command The Command object containing the command to send to AMPS
* @param handler The MessageHandler to invoke to process messages received
* @return the CommandId for the command
* @since 4.0.0.0
* @throws AMPSException Thrown when the command fails.
*/
public CommandId executeAsync(Command command, MessageHandler handler) throws AMPSException {
lock.lock();
try {
return executeAsyncNoLock(command, handler);
} finally {
lock.unlock();
}
}
// This is a bit complicated, but we're basically doing all the stuff we
// need to do here to get the Client's state to reflect the command we're
// about to send -- for example, record the subscription, set a client
// sequence number, add a handler, and determine whether or not to wait for
// a processed ack.
private CommandId executeAsyncNoLock(Command command, MessageHandler messageHandler)
throws AMPSException {
CommandId cid = command.prepare(this);
CommandId subid = command.getSubId();
CommandId qid = command.getQueryId();
int commandEnum = command.getCommand();
int systemAddedAcks = messageHandler == null ? Message.AckType.None : Message.AckType.Processed;
int requestedAcks = command.getAckType();
final boolean isPublishStore = this.publishStore != null && command.needsSequenceNumber();
if (commandEnum == Message.Command.SOW)
systemAddedAcks |= Message.AckType.Completed;
// fixup bookmark and apply ack types as necessary.
String bookmark = command.getBookmark();
if (bookmark != null) {
if (this.bookmarkStore != null) {
if (command.isSubscribe()) {
systemAddedAcks |= Message.AckType.Persisted;
}
Field subIdField = command._message.getSubIdRaw();
if (Client.Bookmarks.MOST_RECENT.equals(bookmark)) {
Field mostRecent = bookmarkStore.getMostRecent(subIdField);
command._message.setBookmark(mostRecent.buffer,
mostRecent.position,
mostRecent.length);
}
else {
if (!Client.Bookmarks.EPOCH.equals(bookmark)
&& !Client.Bookmarks.NOW.equals(bookmark)
&& !subIdField.isNull()
&& !bookmarkStore.getMostRecent(subIdField).toString()
.equals(bookmark)) {
command._message.setBookmark(bookmark);
if (!bookmarkStore.isDiscarded(command._message)) {
if (bookmarkStore.log(command._message) > 0) {
bookmarkStore.discard(command._message);
bookmarkStore.persisted(
subIdField,
command._message.getBookmarkRaw());
}
}
}
}
} else if (Client.Bookmarks.MOST_RECENT.equals(bookmark)) {
command._message.setBookmark(Bookmarks.EPOCH);
}
}
// If this is destined for a publish store, make sure to request a persisted ack
// before we add routes
if (isPublishStore) {
systemAddedAcks |= Message.AckType.Persisted;
}
boolean isSubscribe = command.isSubscribe();
// Make sure we have proper ids or are going to generate them
if (messageHandler != null && cid == null && subid == null
&& qid == null
&& (commandEnum & Message.Command.NoDataCommands) == 0) {
throw new IllegalArgumentException(
"To use a messagehandler, you must also supply a command or subscription ID.");
}
boolean isRegistered = false;
// if there's a command id & a processed ack type set, use syncAckProcessing.
boolean useSyncSend = cid != null && ((systemAddedAcks | requestedAcks) & Message.AckType.Processed) != 0;
try {
if (isPublishStore) {
command._message.setAckType(systemAddedAcks | requestedAcks);
while (true) {
try {
lock.unlock();
publishStore.store(command._message);
} finally {
lock.lock();
}
// Register any message handler supplied if not a subscribe command.
// Subscription handlers will be registered later, when we're sure we
// won't be interfering with resubscription by subscription manager.
if (messageHandler != null
&& !_registerHandler(qid, subid, cid,
messageHandler, requestedAcks,
systemAddedAcks, isSubscribe)) {
if ((commandEnum & Message.Command.NoDataCommands) != 0) {
cid = command.setCommandId(CommandId.nextIdentifier())
.getCommandId();
command._message.setCommandId(cid);
_routes.addRoute(cid, messageHandler, requestedAcks,
systemAddedAcks, isSubscribe);
useSyncSend = ((systemAddedAcks | requestedAcks) & Message.AckType.Processed) != 0;
}
isRegistered = true;
}
long sequence = command._message.getSequence();
command.setClientSequenceNumber(sequence);
if (useSyncSend)
syncAckProcessing(cid, command._message,
command.getTimeout(),
sequence, false);
else
sendInternal(command._message, sequence);
break;
}
} else {
if (isSubscribe) {
// Cache values we'll need, since the command could be
// invalid after syncAckProcessing.
subid = new CommandId(subid.id, 0, subid.id.length);
if (!_defaultSubscriptionManager) {
boolean existingSub = (_routes.findRoute(subid) != null);
try {
Message message = command._message.copy();
if (cid != null)
message.getCommandId(cid);
lock.unlock();
try {
this.subscriptionManager.subscribe(messageHandler,
message);
} finally {
lock.lock();
}
if (cid != null)
message.setCommandId(cid);
message.setAckType(systemAddedAcks | requestedAcks);
if (useSyncSend) {
if (!_registerHandler(qid, subid, cid,
messageHandler,
requestedAcks,
systemAddedAcks,
isSubscribe)) {
command._message.setAckType(requestedAcks);
throw new IllegalArgumentException(
"To use a messagehandler, you must also supply a command or subscription ID.");
}
isRegistered = !existingSub;
syncAckProcessing(cid, message, command.getTimeout(), 0,
!(messageHandler instanceof MessageStream));
} else {
sendInternal(message, 0, !(messageHandler instanceof MessageStream));
}
} catch (AMPSException ex) {
if (!existingSub) {
lock.unlock();
try {
subscriptionManager.unsubscribe(subid);
} finally {
lock.lock();
}
}
throw ex;
}
} else {
command._message.setAckType(systemAddedAcks | requestedAcks);
if (useSyncSend) {
if (!_registerHandler(qid, subid, cid,
messageHandler,
requestedAcks,
systemAddedAcks,
isSubscribe)) {
command._message.setAckType(requestedAcks);
throw new IllegalArgumentException(
"To use a messagehandler, you must also supply a command or subscription ID.");
}
isRegistered = true;
syncAckProcessing(cid, command._message, command.getTimeout(), 0, false);
} else {
sendInternal(command._message, 0, true);
}
}
} else {
// Register any message handler supplied if not a subscribe command.
if (messageHandler != null
&& !_registerHandler(qid, subid, cid,
messageHandler, requestedAcks,
systemAddedAcks, isSubscribe)) {
if ((commandEnum & Message.Command.NoDataCommands) != 0) {
cid = command.setCommandId(CommandId.nextIdentifier())
.getCommandId();
command._message.setCommandId(cid);
_routes.addRoute(cid, messageHandler, requestedAcks,
systemAddedAcks, isSubscribe);
useSyncSend = ((systemAddedAcks | requestedAcks) & Message.AckType.Processed) != 0;
isRegistered = true;
}
} else {
isRegistered = messageHandler != null;
}
command._message.setAckType(systemAddedAcks | requestedAcks);
if (useSyncSend)
syncAckProcessing(cid, command._message, command.getTimeout());
else
sendInternal(command._message);
}
}
} catch (TimedOutException ex) {
// Need to remove any registered handlers and unsub due to failure
// Unless it's saved in the publish store and will be
// resent after a reconnect without restoring the handler.
try {
if (isRegistered && !isPublishStore) {
unsubscribeInternal(cid);
if (qid != null && !cid.equals(qid)) {
unsubscribeInternal(qid);
}
if (subid != null && !cid.equals(subid)) {
unsubscribeInternal(subid);
}
}
} catch (DisconnectedException e) {
ex.addSuppressed(e);
}
throw ex;
} catch (AMPSException ex) {
// Need to remove any registered handlers due to failure
// Unless it's saved in the publish store and will be
// resent after a reconnect without restoring the handler.
if (isRegistered && !isPublishStore) {
_routes.removeRoute(cid);
if (qid != null && !cid.equals(qid)) {
_routes.removeRoute(qid);
}
if (subid != null && !cid.equals(subid)) {
_routes.removeRoute(subid);
}
}
throw ex;
}
command._message.setAckType(requestedAcks);
if (subid != null)
return subid;
if (qid != null)
return qid;
return cid;
}
/**
* Requests a server heartbeat, and configures the client to close the
* connection
* if a heartbeat (or other activity) is not seen on the connection.
*
* @param intervalSeconds_ The time (seconds) between beats from the server.
* @param timeoutSeconds_ The time (seconds) to allow silence on the connection
* before assuming it is dead.
* @throws DisconnectedException The client became disconnected while attempting
* to send the heartbeat request.
*/
public void setHeartbeat(int intervalSeconds_, int timeoutSeconds_) throws DisconnectedException {
_heartbeatInterval = intervalSeconds_;
_readTimeout = Math.max(timeoutSeconds_, intervalSeconds_);
_sendHeartbeat();
}
/**
* Requests a server heartbeat, and configures the client to close the
* connection
* if a heartbeat (or other activity) is not seen on the connection after two
* heartbeat intervals.
*
* @param intervalSeconds_ The time (seconds) between beats from the server.
* @throws DisconnectedException The client became disconnected while attempting
* to send the heartbeat request.
*/
public void setHeartbeat(int intervalSeconds_) throws DisconnectedException {
setHeartbeat(intervalSeconds_, intervalSeconds_ * 2);
}
/**
* Returns the heartbeat interval used by the Client. A value of 0 means
* heartbeats are disabled.
*
* @return The heartbeat interval in seconds.
*/
public int getHeartbeatInterval() {
return _heartbeatInterval;
}
/**
* Returns the time (in seconds) to allow silence on the connection before
* assuming it is dead.
*
* @return The read timeout in seconds.
*/
public int getReadTimeout() {
return _readTimeout;
}
/**
* Sends the cached heartbeat settings to the server in the form of a
* heartbeat command, and configures the read timeout on the socket.
*/
void _sendHeartbeat() throws DisconnectedException {
if (this.transport != null) {
lock.lock();
try {
if (_heartbeatInterval > 0) {
this.message.reset();
this.message.setCommand(Message.Command.Heartbeat);
this.message.setOptions(String.format("start,%d", _heartbeatInterval));
this.heartbeatTimer.setTimeout(_heartbeatInterval * 1000L);
this.heartbeatTimer.start();
this.transport.sendWithoutRetry(this.message);
this.transport.setReadTimeout(_readTimeout * 1000);
broadcastConnectionStateChanged(ConnectionStateListener.HeartbeatInitiated);
}
} finally {
lock.unlock();
}
}
}
/**
* Publish a message to an AMPS topic. If the client
* has a publish store set, then the client will store the message
* before forwarding the message to AMPS. This method does not wait
* for a response from the AMPS server. To detect failure, install
* a failed write handler. If a DisconnectException occurs,
* the message is still stored in the publish store.
*
* @param topic the topic to publish to
* @param topicOffset the offset in topic where the topic begins
* @param topicLength the length of the topic in the topic array
* @param data the data to publish
* @param dataOffset the offset in data where data begins
* @param dataLength the length of the data
* @throws DisconnectedException the client is not connected; the program needs
* to call connect()
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long publish(
byte[] topic, int topicOffset, int topicLength,
byte[] data, int dataOffset, int dataLength)
throws AMPSException {
if (topicLength == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.Publish)
.setTopic(topic, topicOffset, topicLength)
.setData(data, dataOffset, dataLength);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Publish a message to an AMPS topic. If the client
* has a publish store set, then the client will store the message
* before forwarding the message to AMPS. This method does not wait
* for a response from the AMPS server. To detect failure, install
* a failed write handler. If a DisconnectException occurs,
* the message is still stored in the publish store.
*
* @param topic the topic to publish the data to
* @param data the data to publish to the topic
* @throws DisconnectedException the client is not connected; the program needs
* to call connect()
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long publish(
String topic,
String data)
throws AMPSException {
if (topic == null || topic.length() == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.Publish)
.setTopic(topic).setData(data);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Publish a message to an AMPS topic. If the client
* was created with a persistent store on construction, then the client will
* store
* before forwarding the message to AMPS. In this store-and-forward case, the
* persistent
* store will notify the user, when requested via callback, of the successful
* persistence
* of the record within AMPS. If a DisconnectedException occurs, the message is
* still
* stored in the publish store.
*
* @param topic the topic to publish to
* @param topicOffset the offset in topic where the topic begins
* @param topicLength the length of the topic in the topic array
* @param data the data to publish
* @param dataOffset the offset in data where data begins
* @param dataLength the length of the data
* @param expiration the number of seconds until the message expires
* @throws DisconnectedException the client is not connected; the program needs
* to call connect()
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long publish(
byte[] topic, int topicOffset, int topicLength,
byte[] data, int dataOffset, int dataLength,
int expiration)
throws AMPSException {
if (topicLength == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.Publish)
.setTopic(topic, topicOffset, topicLength)
.setData(data, dataOffset, dataLength)
.setExpiration(expiration);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Publish a message to an AMPS topic. If the client was created with a
* persistent store on construction, then the client will store before
* forwarding the message to AMPS. In this store-and-forward case, the
* persistent store will notify the user, when requested via callback,
* of the successful persistence of the record within AMPS. If a
* DisconnectedException occurs, the message is still stored in
* the publish store.
*
* @param topic Topic to publish the data to
* @param data Data to publish to the topic
* @param expiration The number of seconds until the message expires
* @throws DisconnectedException The client is not connected; the program needs
* to call connect()
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long publish(
String topic,
String data,
int expiration)
throws AMPSException {
if (topic == null || topic.length() == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.Publish)
.setTopic(topic)
.setData(data)
.setExpiration(expiration);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Delta publish a message to an AMPS topic. If the client
* has a publish store set, then the client will store the message
* before forwarding the message to AMPS. This method does not wait
* for a response from the AMPS server. To detect failure, install
* a failed write handler. If a DisconnectException occurs,
* the message is still stored in the publish store.
*
* @param topic Topic to publish the data to
* @param topicOffset offset into topic array where the topic name begins
* @param topicLength length of topic array
* @param data Data to publish to the topic
* @param dataOffset offset into data array where the data begins
* @param dataLength length of data array
* @throws DisconnectedException The client was disconnected at time of publish
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long deltaPublish(
byte[] topic, int topicOffset, int topicLength,
byte[] data, int dataOffset, int dataLength)
throws AMPSException {
if (topicLength == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.DeltaPublish)
.setTopic(topic, topicOffset, topicLength)
.setData(data, dataOffset, dataLength);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Delta publish a message to an AMPS topic. If the client
* has a publish store set, then the client will store the message
* before forwarding the message to AMPS. This method does not wait
* for a response from the AMPS server. To detect failure, install
* a failed write handler. If a DisconnectException occurs,
* the message is still stored in the publish store.
*
* @param topic Topic to publish the data to
* @param data Data to publish to the topic
* @throws DisconnectedException The client was disconnected at time of publish
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long deltaPublish(
String topic, String data)
throws AMPSException {
if (topic == null || topic.length() == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.DeltaPublish)
.setTopic(topic)
.setData(data);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Delta publish a message to an AMPS topic. If the client
* has a publish store set, then the client will store the message
* before forwarding the message to AMPS. This method does not wait
* for a response from the AMPS server. To detect failure, install
* a failed write handler. If a DisconnectException occurs,
* the message is still stored in the publish store.
*
* @param topic Topic to publish the data to
* @param topicOffset offset into topic array where the topic name begins
* @param topicLength length of topic array
* @param data Data to publish to the topic
* @param dataOffset offset into data array where the data begins
* @param dataLength length of data array
* @param expiration the number of seconds until the message expires
* @throws DisconnectedException The client was disconnected at time of publish
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long deltaPublish(
byte[] topic, int topicOffset, int topicLength,
byte[] data, int dataOffset, int dataLength, int expiration)
throws AMPSException {
if (topicLength == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.DeltaPublish)
.setTopic(topic, topicOffset, topicLength)
.setData(data, dataOffset, dataLength)
.setExpiration(expiration);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* Delta publish a message to an AMPS topic. If the client
* has a publish store set, then the client will store the message
* before forwarding the message to AMPS. This method does not wait
* for a response from the AMPS server. To detect failure, install
* a failed write handler. If a DisconnectException occurs,
* the message is still stored in the publish store.
*
* @param topic Topic to publish the data to
* @param data Data to publish to the topic
* @param expiration the number of seconds until the message expires
* @throws DisconnectedException The client was disconnected at time of publish
* @throws StoreException An error occurred writing to the local HA
* store.
* @return the sequence number for the message when a persistent store is
* configured for the client, 0 if no store is configured
*/
public long deltaPublish(
String topic, String data, int expiration)
throws AMPSException {
if (topic == null || topic.length() == 0) {
throw new InvalidTopicException("A topic must be specified.");
}
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.DeltaPublish)
.setTopic(topic)
.setData(data)
.setExpiration(expiration);
executeAsync(publishCommand, null);
return publishCommand.getClientSequenceNumber();
}
/**
* @deprecated No longer supported by the AMPS server.
* Sends a start timer command to AMPS, which can be later stopped
* with a stop timer command.
*
* @throws ConnectionException A connection error occurred while sending.
*/
@Deprecated
public void startTimer() throws AMPSException {
lock.lock();
try {
if (serverVersion.compareTo(new VersionInfo("5.3.2")) >= 0) {
throw new CommandException("The start_timer command has been deprecated");
}
executeAsyncNoLock(_command.reset(Message.Command.StartTimer), null);
} finally {
lock.unlock();
}
}
/**
* @deprecated No longer supported by the AMPS server.
* Sends a stop timer command to AMPS
*
* @param handler_ A MessageHandler to process the acknowledgement returned by
* AMPS.
* @return The command identifier.
* @throws ConnectionException A connection error occurred while sending.
*/
@Deprecated
public CommandId stopTimer(MessageHandler handler_) throws AMPSException {
lock.lock();
try {
if (serverVersion.compareTo(new VersionInfo("5.3.2")) >= 0) {
throw new CommandException("The stop_timer command has been deprecated");
}
return executeAsyncNoLock(_command.reset(Message.Command.StopTimer)
.addAckType(Message.AckType.Stats), handler_);
} finally {
lock.unlock();
}
}
/**
* Sets the uninterpreted logon correlation information a client sends
* at logon to aid in searching server log files for specific clients.
*
* @param correlationData The base64 data to be used for logon correlation.
*/
public void setLogonCorrelationData(String correlationData) {
logonCorrelationData = correlationData;
}
/**
* Gets the uninterpreted logon correlation information currently set.
*
* @return The base64 data to be used for logon correlation.
*/
public String getLogonCorrelationData() {
return logonCorrelationData;
}
/**
* Logs into AMPS with the parameters provided in the connect method and the
* logon correlation id, if any, that is set.
*
* @param timeout The number of milliseconds to wait for the client to receive
* and consume a processed ack for this command, where 0
* indicates to wait indefinitely
*
* @return The command identifier.
* @throws ConnectionException A connection error occurred while logging on.
*/
public CommandId logon(long timeout) throws ConnectionException {
return logon(timeout, new DefaultAuthenticator(), null);
}
/**
* Logs into AMPS with the parameters provided in the connect method and the
* logon correlation id, if any, that is set.
*
* @param timeout The number of milliseconds to wait for the client to receive
* and consume a processed ack for this command, where 0
* indicates to wait indefinitely
* @param options The options string to pass with the logon command
*
* @return The command identifier.
* @throws ConnectionException A connection error occurred while logging on.
*/
public CommandId logon(long timeout, String options)
throws ConnectionException {
return logon(timeout, new DefaultAuthenticator(), options);
}
/**
* Logs into AMPS with the parameters provided in the connect method and the
* logon correlation id, if any, that is set.
*
* @param options The options string to pass with the logon command
*
* @return The command identifier.
* @throws ConnectionException A connection error occurred while logging on.
*/
public CommandId logon(String options)
throws ConnectionException {
return logon(0, new DefaultAuthenticator(), options);
}
/**
* Logs into AMPS with the parameters provided in the connect method and the
* logon correlation id, if any, that is set.
*
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @param authenticator The custom authenticator object to authenticate against
* @return The command identifier
* @throws ConnectionException A connection error occurred while logging on.
*/
public CommandId logon(long timeout, Authenticator authenticator)
throws ConnectionException {
return logon(timeout, authenticator, null);
}
/**
* Logs into AMPS with the parameters provided in the connect method and the
* logon correlation id, if any, that is set.
*
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @param authenticator The custom authenticator object to authenticate against
* @param options The options string to pass with the logon command
* @return The command identifier
* @throws ConnectionException A connection error occurred while logging on.
*/
public CommandId logon(long timeout, Authenticator authenticator,
String options) throws ConnectionException {
if (uri == null)
throw new DisconnectedException("Not connected");
if (authenticator == null) {
authenticator = new DefaultAuthenticator();
}
CommandId id = CommandId.nextIdentifier();
lock.lock();
try {
// Only set this true if we have a publish store
this._logonInProgress = true;
this.message.reset();
this.message.setCommand(Message.Command.Logon);
this.message.setCommandId(id);
this.message.setClientName(this.name);
if (options != null) {
this.message.setOptions(options);
}
String uriUserInfo = this.uri.getUserInfo();
if (uriUserInfo != null) {
// parse user info
int upSep = uriUserInfo.indexOf(':');
if (upSep < 0) {
this.username = uriUserInfo;
this.message.setUserId(this.username);
this.message.setPassword(authenticator.authenticate(this.username, null));
} else {
this.username = uriUserInfo.substring(0, upSep);
String passToken = uriUserInfo.substring(upSep + 1);
this.message.setUserId(this.username);
this.message.setPassword(
authenticator.authenticate(this.username, passToken));
}
}
String uriPath = this.uri.getPath();
String[] path = uriPath.substring(1, uriPath.length()).split("/");
if (path.length > 1 && !"amps".equals(path[path.length-1])) {
this.message.setMessageType(path[path.length - 1]);
}
URIProperties properties = new URIProperties(this.uri);
String p = properties.getProperty("pretty");
if (p != null && ("true".equalsIgnoreCase(p) || "t".equalsIgnoreCase(p) || "1".equals(p))) {
this.message.setOptions("pretty");
}
try {
// Versions before 3.0 did not support the Processed ack
// for a logon command.
if (this.version >= Version.AMPS_3) {
this.message.setAckType(Message.AckType.Processed);
String version = Client.getVersion();
if (version != null && version.length() != 0)
this.message.setVersion(version + ":java");
else
this.message.setVersion("notinmanifest:java");
// Set this after authenticate has been called so that
// Authenticator can modify it.
if (logonCorrelationData != null)
this.message.setCorrelationId(logonCorrelationData);
AckResponse response = null;
while (true) {
response = this.syncAckProcessing(id, this.message, timeout);
if (response.state == Message.Status.Retry) {
this.message.setPassword(authenticator.retry(response.username, response.password));
this.username = response.username;
this.message.setUserId(this.username);
} else {
break;
}
}
this.serverVersion = response.serverVersion;
this.nameHash = response.bookmark.substring(0,
response.bookmark.indexOf('|'));
if (bookmarkStore != null) {
bookmarkStore.setServerVersion(this.serverVersion.getOldStyleVersion());
}
authenticator.completed(response.username, response.password, response.reason);
broadcastConnectionStateChanged(ConnectionStateListener.LoggedOn);
// heartbeat must be configured after logon completes.
_sendHeartbeat();
} else {
sendInternal(this.message);
broadcastConnectionStateChanged(ConnectionStateListener.LoggedOn);
}
} catch (CommandException e) {
throw new ConnectionException("A command exception occurred while logging on: " + e, e);
} catch (StoreException storeException) {
throw new ConnectionException("A local store exception occurred while logging on: " + storeException, storeException);
}
try {
if (this.publishStore != null) {
this._logonInProgress = false;
this._reconnecting.signalAll();
this.publishStore.replay(replayer);
broadcastConnectionStateChanged(ConnectionStateListener.PublishReplayed);
}
} catch (StoreException storeException) {
throw new ConnectionException(
"A local store exception occurred while replaying stored messages during logon: " + storeException,
storeException);
}
return id;
} finally {
this._logonInProgress = false;
lock.unlock();
}
}
/**
* Logs into AMPS with the parameters provided in the connect method.
*
* @return The command identifier
* @throws ConnectionException A connection error occurred while logging on.
*/
public CommandId logon() throws ConnectionException {
return logon(0);
}
/**
* Places a bookmark subscription with AMPS.
* Starts replay at the most recent message reported by the BookmarkStore.
* Whenever a disconnection or failover occurs for subscriptions created with
* this method, your application will automatically resubsribe to the message
* after the last message it processed.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param subId The subscription ID. You may optionally provide a
* subscription ID
* to ease recovery scenarios, instead of having the
* system automatically
* generate one for you. When used with the 'replace'
* option, this
* is the subscription to be replaced. With a bookmark
* store,
* this is the subscription ID used for recovery. So, when
* using a persistent bookmark store, provide an explicit
* subscription ID that is consistent across application
* restarts.
* @param bookmark The timestamp or bookmark location at which to start
* the subscription. This parameter can be the bookmark
* assigned by AMPS to a message, one of the special
* values in {@link Client.Bookmarks}, or a string of the
* format YYYYmmddTHHMMSS, as described in the AMPS User's
* guide.
* @param options A {@link Message.Options} value indicating desired
* options for this
* subscription. Use Message.Options.None if no options
* are desired.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId bookmarkSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
CommandId subId,
String bookmark,
String options,
long timeout)
throws AMPSException {
if (bookmark == null) {
throw new AMPSException(
"A bookmark (or one of the Client.Bookmarks constants) is required to initiate a bookmark subscription.");
}
lock.lock();
try {
_command.reset(Message.Command.Subscribe).setTopic(topic)
.setFilter(filter).setOptions(options)
.setBookmark(bookmark).setTimeout(timeout);
if (subId != null)
_command.setSubId(subId);
return executeAsyncNoLock(_command, messageHandler);
} finally {
lock.unlock();
}
}
/**
* Places a subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId subscribe(
MessageHandler messageHandler,
String topic,
String filter,
long timeout)
throws AMPSException {
return subscribe(messageHandler, topic, filter,
Message.Options.None, timeout);
}
/**
* Places a subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId subscribe(MessageHandler messageHandler,
String topic,
String filter,
String options,
long timeout) throws AMPSException {
return subscribe(messageHandler, topic, filter,
options, timeout, null);
}
/**
* Places a subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @param subId The subscription id to use for the subscription.
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId subscribe(MessageHandler messageHandler,
String topic,
String filter,
String options,
long timeout,
String subId) throws AMPSException {
lock.lock();
try {
if (subId == null && options != null && options.contains("replace"))
throw new CommandException(
"Cannot issue a replacement subscription; a valid subscription id is required.");
_command.reset(Message.Command.Subscribe).setTopic(topic)
.setFilter(filter).setOptions(options)
.setTimeout(timeout).setSubId(subId);
return executeAsyncNoLock(_command, messageHandler);
} finally {
lock.unlock();
}
}
/**
* Places a subscription with AMPS.
*
* @param topic The topic to subscribe to
* @return a MessageStream to iterate over.
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public MessageStream subscribe(String topic)
throws AMPSException {
return execute(new Command("subscribe").setTopic(topic));
}
/**
* Places a subscription with AMPS.
*
* @param topic The topic to subscribe to
* @param filter The filter
* @return a MessageStream to iterate over.
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public MessageStream subscribe(String topic, String filter)
throws AMPSException {
return execute(new Command("subscribe").setTopic(topic).setFilter(filter));
}
/**
* Places a subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId subscribe(
MessageHandler messageHandler,
String topic,
long timeout)
throws AMPSException {
return subscribe(messageHandler, topic, null, timeout);
}
/**
* Places a delta subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId deltaSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
String options,
long timeout)
throws AMPSException {
return deltaSubscribe(messageHandler, topic, filter, options, timeout, null);
}
/**
* Places a delta subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @param subId The subscription id to use for the subscription.
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId deltaSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
String options,
long timeout,
String subId)
throws AMPSException {
lock.lock();
try {
_command.reset(Message.Command.DeltaSubscribe)
.setTopic(topic).setFilter(filter)
.setOptions(options).setTimeout(timeout);
if (subId != null)
_command.setSubId(new CommandId(subId));
return executeAsyncNoLock(_command, messageHandler);
} finally {
lock.unlock();
}
}
/**
* Places a delta subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId deltaSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
long timeout)
throws AMPSException {
return deltaSubscribe(messageHandler, topic, filter, Message.Options.None, timeout);
}
/**
* Places a delta subscription with AMPS.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId deltaSubscribe(
MessageHandler messageHandler,
String topic,
long timeout)
throws AMPSException {
return deltaSubscribe(messageHandler, topic, null, timeout);
}
/**
* Remove a subscription from AMPS.
*
* @param subscriptionId The subscription identifier to remove
*
* @throws DisconnectedException The client was disconnected at the time of
* execution. This can still be considered a
* success because the
* subscription was still removed from the
* client's subscription
* manager (if any) and since the client is
* disconnected,
* it is unsubscribed on the server-side.
*/
public void unsubscribe(CommandId subscriptionId) throws DisconnectedException {
if (subscriptionId == null) return;
// Remove from sub mgr before we lock.
this.subscriptionManager.unsubscribe(subscriptionId);
lock.lock();
try {
unsubscribeInternal(subscriptionId);
} finally {
lock.unlock();
}
}
private void unsubscribeInternal(CommandId subscriptionId) throws DisconnectedException {
if (subscriptionId == null) return;
_routes.removeRoute(subscriptionId);
if (this.transport != null) {
this.message.reset();
this.message.setCommand(Message.Command.Unsubscribe);
this.message.setCommandId(CommandId.nextIdentifier());
this.message.setSubId(subscriptionId);
// Try to send unsubscribe command without retrying. If it fails
// that is fine because it means we will be unsubscribed by
// the server. The background thread will need to notice the
// disconnect for any HA reconnect.
this.transport.sendWithoutRetry(this.message);
} else {
throw new DisconnectedException("There is no current connection.");
}
}
/**
* Remove all of the client's subscriptions from AMPS.
*
* @throws DisconnectedException The client was disconnected at the time of
* execution. This can still be considered a
* success because any subscriptions were
* still removed from the client's subscription
* manager (if any) and since the client is
* disconnected, it is unsubscribed on the
* server-side.
*/
public void unsubscribe() throws DisconnectedException {
this.subscriptionManager.clear();
CommandId id = CommandId.nextIdentifier();
lock.lock();
try {
_routes.unsubscribeAll();
if (this.transport != null) {
this.message.reset();
this.message.setCommand(Message.Command.Unsubscribe);
this.message.setCommandId(id);
this.message.setSubId("all");
// Try to send unsubscribe command without retrying. If it fails
// that is fine because it means we will be unsubscribed by
// the server. The background thread will need to notice the
// disconnect for any HA reconnect.
this.transport.sendWithoutRetry(this.message);
} else {
throw new DisconnectedException("There is no current connection.");
}
} finally {
lock.unlock();
}
}
/**
* Synchronously execute a SOW query.
*
* @param topic The topic to query
* @param filter The filter
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public MessageStream sow(String topic, String filter)
throws AMPSException {
return execute(new Command("sow").setTopic(topic).setFilter(filter).setBatchSize(10));
}
/**
* Synchronously execute a SOW query.
*
* @param topic The topic to query
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public MessageStream sow(String topic)
throws AMPSException {
return execute(new Command("sow").setTopic(topic).setBatchSize(10));
}
/**
* Executes a SOW query.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param orderBy The ordering property
* @param bookmark The timestamp or bookmark location for a historical
* query. This parameter can be the bookmark assigned by
* AMPS to a message, one of the special values in
* {@link Client.Bookmarks}, or a string of the format
* YYYYmmddTHHMMSS, as described in the AMPS User's guide.
* @param batchSize The batching parameter to use for the results
* @param topN The maximum number of records the server will return
* (default is all that match)
* @param options The options string for the command, or an empty string
* for no options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sow(MessageHandler messageHandler,
String topic,
String filter,
String orderBy,
String bookmark,
int batchSize,
int topN,
String options,
long timeout) throws AMPSException {
lock.lock();
try {
_command.reset(Message.Command.SOW).setTopic(topic)
.setFilter(filter).setOrderBy(orderBy)
.setOptions(options).setBookmark(bookmark)
.setBatchSize(batchSize).setTopN(topN)
.setTimeout(timeout);
return executeAsyncNoLock(_command, messageHandler);
} finally {
lock.unlock();
}
}
/**
* Executes a SOW query.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the results
* @param options The options string for the command, or an empty string
* for no options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sow(MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
String options,
long timeout) throws AMPSException {
return sow(messageHandler, topic, filter, null, null, batchSize, -1, options, timeout);
}
/**
* Executes a SOW query.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the results
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sow(MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
long timeout) throws AMPSException {
return sow(messageHandler, topic, filter, batchSize, Message.Options.None, timeout);
}
/**
* Executes a SOW query.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param batchSize The batching parameter to use for the results
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sow(MessageHandler messageHandler,
String topic,
int batchSize,
long timeout) throws AMPSException {
return sow(messageHandler, topic, null, batchSize, timeout);
}
/**
* Executes a SOW query.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sow(MessageHandler messageHandler,
String topic,
long timeout) throws AMPSException {
// Default batchsize to 10 for good default performance
return sow(messageHandler, topic, null, 10, timeout);
}
/**
* Executes a SOW query and places a subscription.
*
* @param topic The topic to subscribe to
* @param filter The filter
* @return A MessageStream to iterate over.
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public MessageStream sowAndSubscribe(String topic, String filter)
throws AMPSException {
return execute(new Command("sow_and_subscribe").setTopic(topic).setFilter(filter).setBatchSize(10));
}
/**
* Executes a SOW query and places a subscription.
*
* @param topic The topic to subscribe to
* @return A MessageStream to iterate over.
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public MessageStream sowAndSubscribe(String topic)
throws AMPSException {
return execute(new Command("sow_and_subscribe").setTopic(topic).setBatchSize(10));
}
/**
* Executes a SOW query and places a subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param orderBy The ordering property
* @param bookmark The timestamp or bookmark location for a historical
* query. This parameter can be the bookmark assigned by
* AMPS to a message, one of the special values in
* {@link Client.Bookmarks}, or a string of the format
* YYYYmmddTHHMMSS, as described in the AMPS User's guide.
* @param batchSize The batching parameter to use for the SOW query results
* @param topN The maximum number of records the server will return
* (default is all that match)
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndSubscribe(MessageHandler messageHandler,
String topic,
String filter,
String orderBy,
String bookmark,
int batchSize,
int topN,
String options,
long timeout) throws AMPSException {
lock.lock();
try {
_command.reset(Message.Command.SOWAndSubscribe).setTopic(topic)
.setFilter(filter).setOrderBy(orderBy)
.setOptions(options).setBookmark(bookmark)
.setBatchSize(batchSize).setTopN(topN)
.setTimeout(timeout);
return executeAsyncNoLock(_command, messageHandler);
} finally {
lock.unlock();
}
}
/**
* Executes a SOW query and places a subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the SOW query results
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndSubscribe(MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
String options,
long timeout) throws AMPSException {
return sowAndSubscribe(messageHandler, topic, filter, null, null, batchSize, -1, options, timeout);
}
/**
* Executes a SOW query and places a subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the SOW query results
* @param oofEnabled Specifies whether or not Out-of-Focus processing is
* enabled
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
boolean oofEnabled,
long timeout) throws AMPSException {
return sowAndSubscribe(messageHandler, topic, filter, null, null, batchSize, -1, oofEnabled ? "oof" : null,
timeout);
}
/**
* Executes a SOW query and places a subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the SOW query results
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
long timeout) throws AMPSException {
return sowAndSubscribe(messageHandler, topic, filter, batchSize, false, timeout);
}
/**
* Executes a SOW query and places a subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param batchSize The batching parameter to use for the SOW query results
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndSubscribe(
MessageHandler messageHandler,
String topic,
int batchSize,
long timeout) throws AMPSException {
return sowAndSubscribe(messageHandler, topic, null, batchSize, false, timeout);
}
/**
* Executes a SOW query and places a subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndSubscribe(
MessageHandler messageHandler,
String topic,
long timeout) throws AMPSException {
return sowAndSubscribe(messageHandler, topic, null, 10, false, timeout);
}
/**
* Executes a SOW query and places a delta subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param orderBy The ordering property
* @param batchSize The batching parameter to use for the SOW query results
* @param topN The maximum number of records the server will return
* (default is all that match)
* @param options A value from Message.Options indicating additional
* processing options
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndDeltaSubscribe(MessageHandler messageHandler,
String topic,
String filter,
String orderBy,
int batchSize,
int topN,
String options,
long timeout)
throws AMPSException {
lock.lock();
try {
_command.reset(Message.Command.SOWAndDeltaSubscribe)
.setTopic(topic).setFilter(filter).setOrderBy(orderBy)
.setOptions(options).setBatchSize(batchSize)
.setTopN(topN).setTimeout(timeout);
return executeAsyncNoLock(_command, messageHandler);
} finally {
lock.unlock();
}
}
/**
* Executes a SOW query and places a delta subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the SOW query results
* @param oofEnabled Specifies whether or not Out-of-Focus processing is
* enabled
* @param sendEmpties Specifies whether or not unchanged records are received
* on the delta subscription
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndDeltaSubscribe(MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
boolean oofEnabled,
boolean sendEmpties,
String options,
long timeout)
throws AMPSException {
String opts = options;
if (oofEnabled) {
opts = opts == null ? "oof" : opts + ",oof";
}
if (!sendEmpties) {
opts = opts == null ? "no_empties" : opts + ",no_empties";
}
return sowAndDeltaSubscribe(messageHandler, topic, filter, null, batchSize, -1, opts, timeout);
}
/**
* Executes a SOW query and places a delta subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the SOW query results
* @param oofEnabled Specifies whether or not Out-of-Focus processing is
* enabled
* @param sendEmpties Specifies whether or not unchanged records are received
* on the delta subscription
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndDeltaSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
boolean oofEnabled,
boolean sendEmpties,
long timeout) throws AMPSException {
return sowAndDeltaSubscribe(messageHandler, topic, filter, batchSize, oofEnabled,
sendEmpties, null, timeout);
}
/**
* Executes a SOW query and places a delta subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param filter The filter
* @param batchSize The batching parameter to use for the SOW query results
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndDeltaSubscribe(
MessageHandler messageHandler,
String topic,
String filter,
int batchSize,
long timeout)
throws AMPSException {
return sowAndDeltaSubscribe(messageHandler, topic, filter, batchSize, false, true, timeout);
}
/**
* Executes a SOW query and places a delta subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param batchSize The batching parameter to use for the SOW query results
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndDeltaSubscribe(
MessageHandler messageHandler,
String topic,
int batchSize,
long timeout)
throws AMPSException {
return sowAndDeltaSubscribe(messageHandler, topic, null, batchSize, false, true, timeout);
}
/**
* Executes a SOW query and places a delta subscription.
*
* @param messageHandler The message handler to invoke with matching messages
* @param topic The topic to subscribe to
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws SubscriptionAlreadyExistsException A subscription with this
* identifier is already registered
* @throws BadRegexTopicException The topic specified was an invalid
* regular expression
* @throws TimedOutException The operation took longer than the
* timeout to execute
* @throws DisconnectedException The client wasn't connected when
* the operation was executed
*/
public CommandId sowAndDeltaSubscribe(
MessageHandler messageHandler,
String topic,
long timeout)
throws AMPSException {
return sowAndDeltaSubscribe(messageHandler, topic, null, 10, false, true, timeout);
}
/**
* Executes a SOW delete with filter.
*
* @param messageHandler The message handler to invoke with stats and completed
* acknowledgements
* @param topic The topic to subscribe to
* @param filter The filter. To delete all records, set a filter that is
* always true ('1=1')
* @param options A value from Message.Options indicating additional
* processing options.
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sowDelete(
MessageHandler messageHandler,
String topic,
String filter,
String options,
long timeout)
throws AMPSException {
Command publishCommand = _publishCommand.get();
publishCommand.reset(Message.Command.SOWDelete)
.setTopic(topic).setFilter(filter)
.addAckType(Message.AckType.Stats)
.setOptions(options).setTimeout(timeout);
return executeAsync(publishCommand, messageHandler);
}
/**
* Executes a SOW delete with filter.
*
* @param messageHandler The message handler to invoke with stats and completed
* acknowledgements
* @param topic The topic to subscribe to
* @param filter The filter. To delete all records, set a filter that is
* always true ('1=1')
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sowDelete(
MessageHandler messageHandler,
String topic,
String filter,
long timeout) throws AMPSException {
return this.sowDelete(messageHandler, topic, filter, null, timeout);
}
/**
* Executes a SOW delete with filter.
*
* @param topic The topic to subscribe to
* @param filter The filter. To delete all records, set a filter that is always
* true ('1=1')
* @param timeout The number of milliseconds to wait for the client to receive
* and consume a processed ack for this command, where 0
* indicates to wait indefinitely
* @return The stats message returned by this delete.
* @throws BadFilterException The provided filter is invalid
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public Message sowDelete(String topic,
String filter,
long timeout) throws AMPSException {
MessageStream ms = null;
ms = execute(_publishCommand.get().reset(Message.Command.SOWDelete)
.addAckType(Message.AckType.Stats).setTopic(topic)
.setFilter(filter).setTimeout(timeout));
return (ms == null) ? null : ms.next();
}
/**
* Executes a SOW delete using the SowKey assigned by AMPS to specify
* the messages to delete. When you have the SowKeys for the messages,
* this method is more efficient than using a filter to
* delete messages from the SOW.
*
* For example, to efficiently delete a message that your program has
* received from AMPS:
*
*
*
* // DeleteMessageHandler receives a message containing
* // the results of the delete.
* DeleteMessageHandler dmh = new DeleteMessageHandler();
*
* client.sowDeleteByKeys(dmh, message.getTopic(), message.getSowKey(), 1000);
*
*
*
* @param messageHandler The message handler to invoke with stats and completed
* acknowledgements
* @param topic The topic to execute the SOW delete against
* @param keys A comma separated list of SOW keys to be deleted
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws BadRegexTopicException The topic specified was an invalid regular
* expression
* @throws TimedOutException The operation took longer than the timeout to
* execute
* @throws DisconnectedException The client wasn't connected when the operation
* was executed
*/
public CommandId sowDeleteByKeys(
MessageHandler messageHandler,
String topic,
String keys,
long timeout) throws AMPSException {
return executeAsync(_publishCommand.get()
.reset(Message.Command.SOWDelete)
.setTopic(topic).setSowKeys(keys)
.addAckType(Message.AckType.Stats)
.setTimeout(timeout), messageHandler);
}
/**
* Executes a SOW delete by data. AMPS uses key fields in the data to
* find and delete a message with the same keys.
*
* @param messageHandler The message handler to invoke with stats and completed
* acknowledgements
* @param topic The topic to execute the SOW delete against
* @param data The message to match and delete in the SOW cache
* @param timeout The number of milliseconds to wait for the client to
* receive and consume a processed ack for this command,
* where 0 indicates to wait indefinitely
* @return The command identifier assigned to this command
* @throws AMPSException An error occurred while waiting for this command to be
* processed.
*/
public CommandId sowDeleteByData(
MessageHandler messageHandler,
String topic,
String data,
long timeout) throws AMPSException {
return executeAsync(_publishCommand.get()
.reset(Message.Command.SOWDelete)
.addAckType(Message.AckType.Stats)
.setTopic(topic).setData(data)
.setTimeout(timeout), messageHandler);
}
/**
* Ensures that AMPS messages are sent and have been processed by the
* AMPS server.
* When a publish store is present, waits until all of the messages
* in the publish store at the time the command is called have been
* acknowledged as persisted.
*
* Otherwise, the client issues a flush command and waits for a processed
* acknowledgement. This method blocks until messages have been
* processed or until the client is disconnected, and is most useful when
* the application reaches a point at which it is acceptable to block to
* ensure that messages have been delivered to the AMPS server. For
* example, an application might call publishFlush before exiting.
* One thing to note is that if AMPS is unavailable (HA Client), publishFlush
* needs to wait for a connection to come back up which may look like it's
* hanging.
*
* @throws DisconnectedException Thrown if the client is shut down or has never
* been connected
* @throws TimedOutException Not thrown from this overload: the method
* blocks until the operation completes.
*/
public void publishFlush()
throws DisconnectedException, TimedOutException {
if (publishStore == null) {
try {
lock.lock();
this.message.reset();
CommandId id = CommandId.nextIdentifier();
this.message.setCommandId(id);
this.message.setAckType(Message.AckType.Processed);
this.message.setCommand(Message.Command.Flush);
this.syncAckProcessing(id, message, 0);
return;
} catch (NullPointerException e) {
throw new DisconnectedException("not connected", e);
} catch (AMPSException e) {
absorbedException(e);
} finally {
lock.unlock();
}
} else {
publishStore.flush();
}
}
private static class FlushAckHandler implements MessageHandler, ConnectionStateListener {
public static final int Waiting = 0;
public static final int Acked = 1;
public static final int Disconnected = 2;
public volatile int _status = Waiting;
public FlushAckHandler(Client client_) {
client_.addConnectionStateListener(this);
}
public void connectionStateChanged(int state_) {
if (state_ <= ConnectionStateListener.Shutdown) {
_status = Disconnected;
}
}
public void invoke(Message m) {
_status = Acked;
}
}
private static VersionInfo minPersistedFlushVersion = new VersionInfo("5.3.3");
/**
* Ensures that AMPS messages are sent and have been processed by the
* AMPS server.
* When a publish store is present, waits until all of the messages
* in the publish store at the time the command is called have been
* acknowledged as persisted.
*
* In addition, the behavior depends on the server version.
* the client issues a flush command. If the server
* supports waiting for a persisted acknowledgement indicating that all
* prior messages are also persisted and that was requested, it will do
* that. Otherwise, it waits for a processed acknowledgement.
* This method blocks until messages have been processed or until
* a disconnect occurs, and is most useful when the application reaches a
* point at which it is acceptable to block to ensure that messages
* have been delivered to the AMPS server. For example, an application
* might call publishFlush before exiting.
* One thing to note is that if AMPS is unavailable (HA Client),
* publishFlush needs to wait for a connection to come back up which
* may look like it's hanging.
*
* @param ackType The acknowledgement type that the flush command should wait to
* receive, see {@link Message.AckType}. `processed` matches the
* no arguments version of this function. `persisted` ensures all
* previous messages have been persisted.
* @throws CommandException Thrown if an invalid ack type is requested.
* @throws DisconnectedException Thrown if the client is shut down or has never
* been connected
* @throws TimedOutException Not thrown from this overload: the method
* blocks until the operation completes.
*/
public void publishFlush(String ackType)
throws CommandException, DisconnectedException, TimedOutException {
if (!"persisted".equals(ackType) && !"processed".equals(ackType)) {
throw new CommandException("publishFlush only accepts 'processed' or 'persisted' ack types");
}
if ("persisted".equals(ackType) && this.serverVersion.compareTo(minPersistedFlushVersion) < 0) {
ackType = "processed";
}
CommandId id = CommandId.nextIdentifier();
FlushAckHandler handler = new FlushAckHandler(this);
try {
lock.lock();
if (!this.connected) {
throw new DisconnectedException("Not connected");
}
this.message.reset();
this.message.setCommandId(id);
this.message.setAckType(ackType);
this.message.setCommand(Message.Command.Flush);
if ("persisted".equals(ackType)) {
this.addMessageHandler(id, handler, Message.AckType.Persisted, false);
} else {
this.addMessageHandler(id, handler, Message.AckType.Processed, false);
}
this.send(message);
} catch (NullPointerException e) {
throw new DisconnectedException("not connected", e);
} finally {
lock.unlock();
}
try {
if (publishStore != null) {
publishStore.flush();
}
while (handler._status == FlushAckHandler.Waiting) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
;
}
}
if (publishStore == null
&& handler._status == FlushAckHandler.Disconnected) {
throw new DisconnectedException("Client disconnected waiting for flush acknowledgement");
}
} finally {
removeMessageHandler(id);
removeConnectionStateListener(handler);
}
}
/**
* Ensures that AMPS messages are sent and have been processed by the
* AMPS server.
* When a publish store is present, waits until all of the messages
* in the publish store at the time the command is called have been
* acknowledged as persisted.
*
* Otherwise, the client issues a flush command and waits for a processed
* acknowledgement. This method blocks until messages have been
* processed, until the timeout occurs, or until the client is
* disconnected, and is most useful when
* the application reaches a point at which it is acceptable to block
* to ensure that messages have been delivered to the AMPS server. For
* example, an application might call publishFlush before exiting.
* One thing to note is that if AMPS is unavailable (HA Client), publishFlush
* needs to wait for a connection to come back up which may look like it's
* hanging.
*
* @param timeout Number of milliseconds to wait for flush, where 0 indicates to
* wait indefinitely
* @throws DisconnectedException Thrown if the client is shut down or has never
* been connected
* @throws TimedOutException Thrown if the timeout period expires before the
* method returns
*
*/
public void publishFlush(long timeout)
throws DisconnectedException, TimedOutException {
if (publishStore == null) {
try {
lock.lock();
this.message.reset();
CommandId id = CommandId.nextIdentifier();
this.message.setCommandId(id);
this.message.setAckType(Message.AckType.Processed);
this.message.setCommand(Message.Command.Flush);
this.syncAckProcessing(id, message, timeout);
return;
} catch (NullPointerException e) {
throw new DisconnectedException("not connected", e);
} catch (AMPSException e) {
absorbedException(e);
} finally {
lock.unlock();
}
} else {
publishStore.flush(timeout);
}
}
/**
* Ensures that AMPS messages are sent and have been processed by the
* AMPS server.
* When a publish store is present, waits until all of the messages
* in the publish store at the time the command is called have been
* acknowledged as persisted.
*
* In addition, the behavior depends on the server version.
* the client issues a flush command. If the server
* supports waiting for a persisted acknowledgement indicating that all
* prior messages are also persisted and that was requested, it will do
* that. Otherwise, it waits for a processed acknowledgement.
* This method blocks until messages have been processed, until the
* timeout has expired, or until a disconnect occurs, and is most useful
* when the application reaches a point at which it is acceptable to block
* to ensure that messages have been delivered to the AMPS server. For
* example, an application might call publishFlush before exiting.
* One thing to note is that if AMPS is unavailable (HA Client),
* publishFlush needs to wait for a connection to come back up which
* may look like it's hanging.
*
* @param ackType The acknowledgement type that the flush command should wait to
* receive, see {@link Message.AckType}. `processed` matches the
* timeout only version of this function. `persisted` ensures all
* previous messages have been persisted.
* @param timeout Number of milliseconds to wait for flush, where 0 indicates to
* wait indefinitely
* @throws CommandException Thrown if an invalid ack type is requested.
* @throws DisconnectedException Thrown if the client is shut down or has never
* been connected
* @throws TimedOutException Not thrown from this overload: the method
* blocks until the operation completes.
*/
public void publishFlush(String ackType, long timeout)
throws CommandException, DisconnectedException, TimedOutException {
if (!"persisted".equals(ackType) && !"processed".equals(ackType)) {
throw new CommandException("publishFlush only accepts processed or persisted ack types");
}
if ("persisted".equals(ackType) && this.serverVersion.compareTo(minPersistedFlushVersion) < 0) {
ackType = "processed";
}
CommandId id = CommandId.nextIdentifier();
FlushAckHandler handler = new FlushAckHandler(this);
try {
lock.lock();
if (!this.connected) {
throw new DisconnectedException("Not connected");
}
this.message.reset();
this.message.setCommandId(id);
this.message.setAckType(ackType);
this.message.setCommand(Message.Command.Flush);
if ("persisted".equals(ackType)) {
this.addMessageHandler(id, handler, Message.AckType.Persisted, false);
} else {
this.addMessageHandler(id, handler, Message.AckType.Processed, false);
}
this.send(message);
} catch (NullPointerException e) {
throw new DisconnectedException("not connected", e);
} finally {
lock.unlock();
}
long end = System.currentTimeMillis() + timeout;
try {
if (publishStore != null) {
publishStore.flush(timeout);
}
while (handler._status == FlushAckHandler.Waiting
&& (timeout == 0 || System.currentTimeMillis() < end)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
;
}
}
if (publishStore == null
&& handler._status == FlushAckHandler.Disconnected) {
throw new DisconnectedException("Client disconnected waiting for flush acknowledgement");
}
if (publishStore == null
&& handler._status != FlushAckHandler.Acked) {
throw new TimedOutException("Timed out waiting for flush acknowledgement");
}
} finally {
removeMessageHandler(id);
removeConnectionStateListener(handler);
}
}
/**
* @deprecated As of 5.3.0.0. Use (@link #publishFlush) instead.
* Clear the queued messages which may be waiting in the transport.
* This is a no-op.
*
* @return long Number of messages or bytes queued after flush is completed or
* timed out
* @throws DisconnectedException Thrown if the client is shut down or has never
* been connected
*
*/
@Deprecated
public long flush() throws DisconnectedException {
// No-op
return 0;
}
/**
* @deprecated As of 5.3.0.0. Use (@link #publishFlush) instead.
* Clear the queued messages which may be waiting in the transport.
* This is a no-op.
*
* @param timeout Number of milliseconds to wait for flush, where 0 indicates to
* wait indefinitely
* @return long Number of messages or bytes queued after flush is completed or
* timed out
* @throws DisconnectedException Thrown if the client is shut down or has never
* been connected
*
*/
@Deprecated
public long flush(long timeout) throws DisconnectedException {
// No-op
return 0;
}
/**
* Return the build number for the client that is stored in the Manifest.mf of
* the jar file.
*
* @return String Build version number.
*/
static public String getVersion() {
Manifest manifest;
if (_version != null)
return _version;
// If it still exists, give precedence to the client version # from the
// JAR manifest first, but use the ClientVersion class as a backup.
// The JAR client version can be lost due to repackaging, such is
// the case with "fat JARs".
String version = "unknown";
try {
// Allow ClientVersion not to exist.
@SuppressWarnings("rawtypes")
Class clientVersionCls = Class.forName(
"com.crankuptheamps.client.ClientVersion");
@SuppressWarnings("unchecked")
Method getImplementationVersion = clientVersionCls.getMethod(
"getImplementationVersion");
version = (String) getImplementationVersion.invoke(null);
} catch (Exception e) {
// Do nothing. Keep using "unknown" as the backup version.
}
// Get the class loader and all jar manifests
ClassLoader cl = (ClassLoader) Client.class.getClassLoader();
if (cl == null) {
_version = version;
return _version;
}
Enumeration urls;
try {
urls = cl.getResources("META-INF/MANIFEST.MF");
} catch (IOException e) {
_version = version;
return _version;
}
URL url = null;
// Search for the amps_client.jar manifest and retrieve Release-Version
while (urls.hasMoreElements()) {
url = urls.nextElement();
if (url.toString().contains("amps_client")) {
try {
manifest = new Manifest(url.openStream());
} catch (IOException E) {
_version = version;
return _version;
}
Attributes attrs = manifest.getMainAttributes();
version = (String) (attrs.getValue("Implementation-Version"));
break;
}
}
_version = version;
return _version;
}
/**
* Get the handler that is invoked immediately by any thread created by
* the transport.
*
* @return The ThreadCreatedHandler invoked by new transport threads.
*/
public ThreadCreatedHandler getThreadCreatedHandler() {
return _threadCreatedHandler;
}
/**
* Set a handler that is invoked immediately by any thread created by
* the transport.
*
* @param handler_ A ThreadCreatedHandler to be invoked by new
* transport threads.
*/
public void setThreadCreatedHandler(ThreadCreatedHandler handler_) {
_threadCreatedHandler = handler_;
if (transport != null) {
transport.setThreadCreatedHandler(_threadCreatedHandler);
}
}
/**
* Get the filter that is used by the Client's transport for all incoming
* and outgoing messages.
*
* @return A TransportFilter invoked by the transport.
*/
public TransportFilter getTransportFilter() {
return _transportFilter;
}
/**
* Set a filter that is used by the Client's transport for all incoming
* and outgoing messages.
*
* @param filter_ A TransportFilter to be invoked by the transport.
*/
public void setTransportFilter(TransportFilter filter_) {
if (filter_ != null) {
_transportFilter = filter_;
} else {
_transportFilter = new DefaultTransportFilter();
}
if (transport != null) {
transport.setTransportFilter(_transportFilter);
}
}
/**
* Assembles a new ConnectionInfo with the state of this client and
* associated classes at the point in time this is called. This
* object is not updated once it is returned.
*
* @return A new ConnectionInfo object.
*/
public ConnectionInfo getConnectionInfo() {
ConnectionInfo ci = new ConnectionInfo();
ci.put("client.uri", this.uri == null ? null : this.uri.toString());
ci.put("client.name", this.name);
ci.put("client.username", this.username);
if (this.publishStore != null) {
ci.put("publishStore.unpersistedCount", this.publishStore.unpersistedCount());
}
return ci;
}
// Queue acking functions
private int _ack(QueueBookmarks bookmarks) throws AMPSException {
// called with lock taken
if (bookmarks._bookmarkCount > 0) {
if (bookmarks._length == 0 || bookmarks._data == null) {
bookmarks._length = 0;
bookmarks._bookmarkCount = 0;
return 0;
}
// Because of the unlock to call store(), we need to copy data from bookmarks
// while the lock is taken. Other bookmarks could get added while the lock is
// released, or another thread could call _ack and cause a double-send. We'll
// reset length and count to 0 and get a copy of the buffer before we release
// the lock.
int bookmarksLength = bookmarks._length;
bookmarks._length = 0;
bookmarks._bookmarkCount = 0;
Command publishCommand = _publishCommand.get();
if (publishCommand._message == null)
publishCommand._message = allocateMessage();
publishCommand._message.reset();
publishCommand._message.setCommand(Message.Command.SOWDelete);
publishCommand._message.setCommandId(_queueAckCommandId);
publishCommand._message.getTopicRaw().set(bookmarks._topic, 0,
bookmarks._topic.length);
byte[] bookmarksData = _bookmarksData.get();
if (bookmarksData.length < bookmarksLength) {
bookmarksData = new byte[bookmarksLength * 2];
_bookmarksData.set(bookmarksData);
}
System.arraycopy(bookmarks._data, 0, bookmarksData, 0, bookmarksLength);
publishCommand._message.setBookmark(bookmarksData, 0, bookmarksLength);
long sequence = 0;
if (this.publishStore != null) {
publishCommand._message.setAckType(Message.AckType.Persisted);
try {
lock.unlock();
publishStore.store(publishCommand._message);
if (!publishCommand._message.isSequenceNull())
sequence = publishCommand._message.getSequence();
} finally {
lock.lock();
}
}
sendInternal(publishCommand._message, sequence);
return 1;
}
return 0;
}
/**
* ACKs a message from a message queue with a byte-array topic and bookmark
*
* @param topic The bytes of the topic to ack
* @param topicPos The starting position in topic
* @param topicLen The length of the topic in bytes
* @param bookmark The bytes of the bookmark to ack
* @param bookmarkPos The starting position in bookmark
* @param bookmarkLen The length of the bookmark
* @throws AMPSException An exception occurred while sending a sow_delete
* message.
*/
public void ack(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException {
if (_isAutoAckEnabled || bookmark == null || bookmarkLen == 0)
return;
if (_ackBatchSize < 2) {
_ackImmediate(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen, null, 0, 0);
} else {
_ackMany(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen);
}
}
private void _ackOne(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException {
try {
lock.lock();
_hashBox.set(topic, topicPos, topicLen);
QueueBookmarks bookmarks = _topicHashMap.get(_hashBox);
if (bookmarks == null) {
bookmarks = new QueueBookmarks();
bookmarks._topic = new byte[topicLen];
System.arraycopy(topic, topicPos, bookmarks._topic, 0, topicLen);
bookmarks._data = new byte[512];
_topicHashMap.put(_hashBox.clone(), bookmarks);
}
int bookmarksCapacity = bookmarks._data.length;
int newLength = bookmarks._length + bookmarkLen + 1;
if (bookmarksCapacity < newLength) {
bookmarksCapacity = 512 * (1 + (newLength / 512));
byte[] newData = new byte[bookmarksCapacity];
System.arraycopy(bookmarks._data, 0, newData, 0, bookmarks._length);
bookmarks._data = newData;
}
if (bookmarks._length > 0) {
bookmarks._data[bookmarks._length] = (byte) ',';
++bookmarks._length;
} else {
bookmarks._oldestTime = System.currentTimeMillis();
}
System.arraycopy(bookmark, bookmarkPos, bookmarks._data,
bookmarks._length, bookmarkLen);
bookmarks._length += bookmarkLen;
if (++bookmarks._bookmarkCount >= _ackBatchSize)
_ack(bookmarks);
} finally {
lock.unlock();
}
}
private void _ackMany(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException {
try {
lock.lock();
_hashBox.set(topic, topicPos, topicLen);
QueueBookmarks bookmarks = _topicHashMap.get(_hashBox);
if (bookmarks == null) {
bookmarks = new QueueBookmarks();
bookmarks._topic = new byte[topicLen];
System.arraycopy(topic, topicPos, bookmarks._topic, 0, topicLen);
bookmarks._data = new byte[512];
_topicHashMap.put(_hashBox.clone(), bookmarks);
}
int start = bookmarkPos;
int end = start + 1;
for (; end < bookmarkPos + bookmarkLen; ++end) {
if (bookmark[end] == (byte) ',') {
if (++bookmarks._bookmarkCount >= _ackBatchSize) {
int bookmarksCapacity = bookmarks._data.length;
int newLength = bookmarks._length + end - start + 1;
if (bookmarksCapacity < newLength) {
bookmarksCapacity = 512 * (1 + (newLength / 512));
byte[] newData = new byte[bookmarksCapacity];
System.arraycopy(bookmarks._data, 0, newData, 0, bookmarks._length);
bookmarks._data = newData;
}
if (bookmarks._length > 0) {
bookmarks._data[bookmarks._length] = (byte) ',';
++bookmarks._length;
} else {
bookmarks._oldestTime = System.currentTimeMillis();
}
System.arraycopy(bookmark, start, bookmarks._data,
bookmarks._length, end - start);
bookmarks._length += end - start;
_ack(bookmarks);
start = ++end;
}
}
}
// Do we have uncopied bookmark(s)?
if (end - start >= 4) {
int bookmarksCapacity = bookmarks._data.length;
int newLength = bookmarks._length + end - start + 1;
if (bookmarksCapacity < newLength) {
bookmarksCapacity = 512 * (1 + (newLength / 512));
byte[] newData = new byte[bookmarksCapacity];
System.arraycopy(bookmarks._data, 0, newData, 0, bookmarks._length);
bookmarks._data = newData;
}
if (bookmarks._length > 0) {
bookmarks._data[bookmarks._length] = (byte) ',';
++bookmarks._length;
} else {
bookmarks._oldestTime = System.currentTimeMillis();
}
System.arraycopy(bookmark, start, bookmarks._data,
bookmarks._length, end - start);
bookmarks._length += end - start;
if (++bookmarks._bookmarkCount >= _ackBatchSize)
_ack(bookmarks);
}
} finally {
lock.unlock();
}
}
private void _ackImmediate(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen,
byte[] options, int optionsPos, int optionsLen) throws AMPSException {
Command publishCommand = _publishCommand.get();
if (publishCommand._message == null)
publishCommand._message = allocateMessage();
publishCommand._message.reset();
publishCommand._message.setCommand(Message.Command.SOWDelete);
publishCommand._message.setCommandId(_queueAckCommandId);
publishCommand._message.getTopicRaw().set(topic,
topicPos,
topicLen);
publishCommand._message.getBookmarkRaw().set(bookmark,
bookmarkPos,
bookmarkLen);
if (options != null && optionsLen > 0) {
publishCommand._message.getOptionsRaw().set(options,
optionsPos,
optionsLen);
}
long sequence = 0;
if (this.publishStore != null) {
publishCommand._message.setAckType(Message.AckType.Persisted);
publishStore.store(publishCommand._message);
if (!publishCommand._message.isSequenceNull())
sequence = publishCommand._message.getSequence();
}
if (sequence > 0 || options == null ||
!CANCEL_FIELD.equals(publishCommand._message.getOptionsRaw())) {
try {
lock.lock();
sendInternal(publishCommand._message, sequence);
} finally {
lock.unlock();
}
} else {
this.transport.sendWithoutRetry(publishCommand._message);
}
return;
}
/**
* ACKs a message queue message using {@link Field} objects from a message.
*
* @param topic The topic to ack
* @param bookmark The bookmark to ack
* @throws AMPSException An error occurred while sending a sow_delete message.
*/
public void ack(Field topic, Field bookmark) throws AMPSException {
ack(topic.buffer, topic.position, topic.length,
bookmark.buffer, bookmark.position, bookmark.length);
}
/**
* ACKs a message queue message.
*
* @param topic The topic to ack
* @param bookmark The bookmark or comma-delimited bookmark list to ack.
* @throws AMPSException An error occurred while sending a sow_delete message.
*/
public void ack(String topic, String bookmark) throws AMPSException {
if (bookmark == null || bookmark.length() == 0 || _isAutoAckEnabled)
return;
if (_ackBatchSize < 2) {
Command publishCommand = _publishCommand.get();
if (publishCommand._message == null)
publishCommand._message = allocateMessage();
publishCommand._message.reset();
publishCommand._message.setCommand(Message.Command.SOWDelete);
publishCommand._message.setCommandId(_queueAckCommandId);
publishCommand._message.setTopic(topic);
publishCommand._message.setBookmark(bookmark);
long sequence = 0;
if (this.publishStore != null) {
publishCommand._message.setAckType(Message.AckType.Persisted);
publishStore.store(publishCommand._message);
if (!publishCommand._message.isSequenceNull())
sequence = publishCommand._message.getSequence();
}
try {
lock.lock();
sendInternal(publishCommand._message, sequence);
} finally {
lock.unlock();
}
return;
}
try {
lock.lock();
_hashBox.set(topic);
QueueBookmarks bookmarks = _topicHashMap.get(_hashBox);
if (bookmarks == null) {
bookmarks = new QueueBookmarks();
bookmarks._topic = topic.getBytes(StandardCharsets.UTF_8);
bookmarks._data = new byte[512];
_topicHashMap.put(_hashBox.clone(), bookmarks);
}
int bookmarksCapacity = bookmarks._data.length;
int newLength = bookmarks._length + bookmark.length() + 1;
if (bookmarksCapacity < newLength) {
bookmarksCapacity = 512 * (1 + (newLength / 512));
byte[] newData = new byte[bookmarksCapacity];
System.arraycopy(bookmarks._data, 0, newData, 0, bookmarks._length);
bookmarks._data = newData;
}
if (bookmarks._length > 0) {
bookmarks._data[bookmarks._length] = (byte) ',';
++bookmarks._length;
} else {
bookmarks._oldestTime = System.currentTimeMillis();
}
for (int i = 0; i < bookmark.length(); ++i) {
bookmarks._data[bookmarks._length] = (byte) bookmark.charAt(i);
if (bookmarks._data[bookmarks._length++] == (byte) ',') {
if (++bookmarks._bookmarkCount >= _ackBatchSize) {
_ack(bookmarks);
}
}
}
if (++bookmarks._bookmarkCount >= _ackBatchSize) {
_ack(bookmarks);
}
} finally {
lock.unlock();
}
}
/**
* ACKs a message from a message queue with a byte-array topic and bookmark
*
* @param topic The bytes of the topic to ack
* @param topicPos The starting position in topic
* @param topicLen The length of the topic in bytes
* @param bookmark The bytes of the bookmark to ack
* @param bookmarkPos The starting position in bookmark
* @param bookmarkLen The length of the bookmark
* @param options The bytes of the options for the ack
* @param optionsPos The starting position in options
* @param optionsLen The length of the options
* @throws AMPSException An exception occurred while sending a sow_delete
* message.
*/
public void ack(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen,
byte[] options, int optionsPos, int optionsLen) throws AMPSException {
if (_isAutoAckEnabled)
return;
if (_ackBatchSize < 2 || (options != null && optionsLen > 0)) {
_ackImmediate(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen,
options, optionsPos, optionsLen);
} else {
_ackMany(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen);
}
}
protected void _ack(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen,
byte[] options, int optionsPos, int optionsLen) throws AMPSException {
if (bookmark == null || bookmarkLen == 0)
return;
if (_ackBatchSize < 2 || (options != null && optionsLen > 0)) {
_ackImmediate(topic, topicPos, topicLen, bookmark, bookmarkPos,
bookmarkLen, options, optionsPos, optionsLen);
} else {
_ackOne(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen);
}
}
/**
* ACKs a message queue message using {@link Field} objects from a message.
*
* @param topic The topic to ack
* @param bookmark The bookmark to ack
* @param options The options for the ack
* @throws AMPSException An error occurred while sending a sow_delete message.
*/
public void ack(Field topic, Field bookmark, Field options) throws AMPSException {
if (_isAutoAckEnabled)
return;
ack(topic.buffer, topic.position, topic.length,
bookmark.buffer, bookmark.position, bookmark.length,
options.buffer, options.position, options.length);
}
/**
* ACKs a message queue message.
*
* @param topic The topic to ack
* @param bookmark The bookmark or comma-delimited bookmark list to ack.
* @param options The options for the ack
* @throws AMPSException An error occurred while sending a sow_delete message.
*/
public void ack(String topic, String bookmark, String options) throws AMPSException {
if (bookmark == null || bookmark.length() == 0 || _isAutoAckEnabled)
return;
if (_ackBatchSize < 2 ||
(options != null && options.length() > 0)) {
Command publishCommand = _publishCommand.get();
if (publishCommand._message == null)
publishCommand._message = allocateMessage();
publishCommand._message.reset();
publishCommand._message.setCommand(Message.Command.SOWDelete);
publishCommand._message.setCommandId(_queueAckCommandId);
publishCommand._message.setTopic(topic);
publishCommand._message.setBookmark(bookmark);
if (options != null && options.length() > 0) {
publishCommand._message.setOptions(options);
}
long sequence = 0;
if (this.publishStore != null) {
publishCommand._message.setAckType(Message.AckType.Persisted);
publishStore.store(publishCommand._message);
if (!publishCommand._message.isSequenceNull())
sequence = publishCommand._message.getSequence();
}
if (sequence > 0 ||
!Message.Options.Cancel.equals(options)) {
try {
lock.lock();
sendInternal(publishCommand._message, sequence);
} finally {
lock.unlock();
}
} else // a cancel should only send during an active session
{
sendWithoutRetry(publishCommand._message);
}
return;
}
try {
lock.lock();
_hashBox.set(topic);
QueueBookmarks bookmarks = _topicHashMap.get(_hashBox);
if (bookmarks == null) {
bookmarks = new QueueBookmarks();
bookmarks._topic = topic.getBytes(StandardCharsets.UTF_8);
bookmarks._data = new byte[512];
_topicHashMap.put(_hashBox.clone(), bookmarks);
}
int bookmarksCapacity = bookmarks._data.length;
if (bookmarksCapacity < bookmarks._length + 1 + bookmark.length()) {
byte[] newData = new byte[2 * bookmarksCapacity];
System.arraycopy(bookmarks._data, 0, newData, 0, bookmarks._length);
bookmarks._data = newData;
}
if (bookmarks._length > 0) {
bookmarks._data[bookmarks._length] = (byte) ',';
++bookmarks._length;
} else {
bookmarks._oldestTime = System.currentTimeMillis();
}
for (int i = 0; i < bookmark.length(); ++i) {
bookmarks._data[bookmarks._length] = (byte) bookmark.charAt(i);
if (bookmarks._data[bookmarks._length++] == (byte) ',') {
if (++bookmarks._bookmarkCount >= _ackBatchSize) {
_ack(bookmarks);
}
}
}
if (++bookmarks._bookmarkCount >= _ackBatchSize) {
_ack(bookmarks);
}
} finally {
lock.unlock();
}
}
/**
* Used internally for autoacking and by Message.
*
* @param topic The topic to ack
* @param bookmark The bookmark to ack
* @throws AMPSException An error occurred while sending a sow_delete message.
*/
protected void _ack(Field topic, Field bookmark) throws AMPSException {
_ack(topic.buffer, topic.position, topic.length,
bookmark.buffer, bookmark.position, bookmark.length, null, 0, 0);
}
/**
* Used internally for autoacking and by Message.
*
* @param topic The topic to ack
* @param bookmark The bookmark to ack
* @param options The options for the ack
* @throws AMPSException An error occurred while sending a sow_delete message.
*/
protected void _ack(Field topic, Field bookmark, Field options) throws AMPSException {
_ack(topic.buffer, topic.position, topic.length,
bookmark.buffer, bookmark.position, bookmark.length,
options.buffer, options.position, options.length);
}
/**
* Enables or disables auto-acking. When auto-acking is enabled any successful
* return from a message handler function will result in an ACK (sow_delete)
* message sent to the server. Any thrown exception will result in a "cancel"
* ACK sent to the server.
*
* @param isAutoAckEnabled true to enable auto-acking; false to disable.
* (default: disabled).
*/
public void setAutoAck(boolean isAutoAckEnabled) {
_isAutoAckEnabled = isAutoAckEnabled;
}
/**
* Returns the current setting of auto-acking.
*
* @return true if auto-acking is enabled, false if auto-acking is disabled.
*/
public boolean getAutoAck() {
return _isAutoAckEnabled;
}
/**
* Sets the current ACK batch size. The ACK batch size controls how many
* successful ACKs (which are sow_delete messages) are batched together
* before sending to the server. When combined with the "max_backlog"
* and MaxPerSubscriptionBacklog server configuration parameter, greater
* network efficiency can be achieved when using message queues. Setting
* this parameter causes calls to "ack" and successful auto-acks to be
* batched; unsuccessful/cancel acks are sent immediately.
*
* @param batchSize The batch size to use (default: 1)
*/
public void setAckBatchSize(int batchSize) {
if (batchSize > 1 && _queueAckTimeout <= 0)
_queueAckTimeout = 1000;
_ackBatchSize = batchSize;
}
/**
* Returns the current ACK batch size. See the documentation for
* {@link #setAckBatchSize}
* for more information on this setting.
*
* @return The current ACK batch size
*/
public int getAckBatchSize() {
return _ackBatchSize;
}
/**
* Sets the ack timeout -- the maximum time to let a success ack be cached
* before sending.
*
* @param ackTimeout The maximum time in milliseconds to wait to acknowledge
* messages, which must be greater than 0 if the ack
* batch size is greater than 1.
*/
public void setAckTimeout(long ackTimeout) {
if (ackTimeout > 0 || (ackTimeout == 0 && _ackBatchSize == 1)) {
_queueAckTimeout = ackTimeout;
}
}
/**
* Returns the current queue ack timeout in milliseconds.
*
* @return The current queue ack timeout, in milliseconds.
*/
public long getAckTimeout() {
return _queueAckTimeout;
}
/**
* Send the current set of queue acknowledgements ("sow_delete" with the message
* bookmark) to the AMPS server. This method sends acknowledgements for messages
* that have been received and acknowledged: it does not acknowledge messages
* that the application has not yet processed.
*
* @throws AMPSException Thrown when the client is unable to flush
* acknowledgements.
*/
public void flushAcks() throws AMPSException {
int sendCount = 0;
// Only need the lock to send the acks
lock.lock();
try {
Collection entries = _topicHashMap.values();
for (Iterator iterator = entries.iterator(); iterator.hasNext();) {
sendCount += _ack(iterator.next());
}
} finally {
lock.unlock();
}
if (sendCount > 0) {
// This locks as needed
publishFlush();
}
}
/**
* This is a helper function used to encapsulate the logic for waiting on a
* sent message so that async commands like subscribe
and
* sow
can have an easier time using an exception interface for
* failed command execution.
*
* @param m Message to be sent
* @param timeout Timeout in milliseconds to wait for the client to receive and
* consume a processed acknowledgement for this command, where 0
* indicates to wait indefinitely
* @throws DisconnectedException Connection was disconnected when
* trying to send
* @throws BadFilterException Bad filter specified in query or
* subscription
* @throws BadRegexTopicException Invalid regex topic specified in
* query or subscription
* @throws SubscriptionAlreadyExistsException Subscription with the specified id
* already exists
* @throws TimedOutException Operation took too long to execute
* @throws NameInUseException Client name is already in use
* @throws AuthenticationException Client was unable to be
* authenticated
* @throws NotEntitledException Client not entitled to that
* command or topic
* @throws SubidInUseException The specified subid is already
* used by the client and this is not
* a replace, pause, or resume of
* that subscription
* @throws CommandException An error with the command was
* returned by the server
* @throws StoreException There was an error saving the
* command to the publish store
*/
private AckResponse syncAckProcessing(CommandId id, Message m, long timeout)
throws DisconnectedException,
TimedOutException,
NameInUseException,
AuthenticationException,
NotEntitledException,
CommandException,
StoreException {
return syncAckProcessing(id, m, timeout, 0, false);
}
/**
* This is a helper function used to encapsulate the logic for waiting
* on a sent message so that async commands like subscribe
* and sow
can have an easier time using an exception
* interface for failed command execution.
*
* @param m Message to be sent
* @param timeout Timeout in milliseconds to wait for the client to receive
* and consume a processed acknowledgement for this command,
* where 0 indicates to wait indefinitely
* @param haSeq The HA sequence number of the message.
* @param isSubscribe If the command is an HA subscribe variant or not.
* @throws DisconnectedException Connection was disconnected when
* trying to send
* @throws BadFilterException Bad filter specified in query or
* subscription
* @throws BadRegexTopicException Invalid regex topic specified in
* query or subscription
* @throws InvalidTopicException Invalid topic specified, either
* the topic isn't a SOW topic for a
* SOW operation or the topic is not
* in the transaction log for a
* bookmark subscription
* @throws SubscriptionAlreadyExistsException Subscription with the specified id
* already exists
* @throws TimedOutException Operation took too long to execute
* @throws NameInUseException Client name is already in use
* @throws AuthenticationException Client was unable to be
* authenticated
* @throws NotEntitledException Client not entitled to that
* command or topic
* @throws SubidInUseException The specified subid is already
* used by the client and this is not
* a replace, pause, or resume of
* that subscription
* @throws CommandException An error with the command was
* returned by the server
* @throws StoreException There was an error saving the
* command to the publish store
*/
private AckResponse syncAckProcessing(CommandId id, Message m,
long timeout, long haSeq,
boolean isSubscribe)
throws DisconnectedException,
TimedOutException,
NameInUseException,
AuthenticationException,
NotEntitledException,
CommandException,
StoreException {
// NOTE: This is called with lock already held
// Make an empty ack response and stick it in the map.
// The reader thread takes care of setting it, removing it from the map,
// and signaling everyone when an ack comes in.
AckResponse ackResponse = new AckResponse();
ackResponse.responded = false;
// This is the only place that should have both locks, but can't
// release the client lock until after send to protect the message
// from being changed.
acksLock.lock();
try {
_acks.put(id, ackResponse);
} finally {
acksLock.unlock();
}
ackResponse.connectionVersion = sendInternal(m, haSeq, isSubscribe);
if (ackResponse.connectionVersion == -1) {
acksLock.lock();
try {
_acks.remove(id);
} finally {
acksLock.unlock();
}
return ackResponse;
}
lock.unlock();
try {
long startTime = System.currentTimeMillis();
long now = startTime;
acksLock.lock();
try {
// First check to see that our connection version hasn't
// already failed / been canceled by a client background
// thread that detected a disconnect.
if (!ackResponse.abandoned
&& ackResponse.connectionVersion <= _lastFailedConnectionVersion) {
ackResponse.abandoned = true;
_acks.remove(id);
}
while (!ackResponse.abandoned && !ackResponse.responded &&
(timeout == 0 || (now - startTime) < timeout)) {
try {
if (timeout > 0) {
long remainingTime = Long.min(1000, timeout - (now - startTime));
if (remainingTime > 0) {
ackReceived.await(remainingTime,
TimeUnit.MILLISECONDS);
}
} else {
ackReceived.await();
}
} catch (InterruptedException ie) {
// It's OK to be interrupted, let's just continue
}
now = System.currentTimeMillis();
}
} finally {
acksLock.unlock();
}
} finally {
lock.lock();
}
if (ackResponse.responded) {
// We found the ack in the callback, therefore we have
// the status and failure reason when they're set.
if (ackResponse.state != Message.Status.Failure) {
if (m.getCommand() == Message.Command.Logon) {
// if we're a 'logon' command we need to extract the seq number,
// if available
if (this.publishStore != null) {
try {
this.publishStore.discardUpTo(ackResponse.sequence);
}
catch (StoreException e) {
absorbedException(e);
throw e;
}
// On a fresh client, we need to adopt the sequence
// number the server has for us or the last persisted value
// from our store.
long lastPersisted = publishStore.getLastPersisted();
if (lastSentSequenceNumber <= lastPersisted) {
lastSentSequenceNumber = lastPersisted;
}
} else if (lastSentSequenceNumber < ackResponse.sequence) {
lastSentSequenceNumber = ackResponse.sequence;
}
}
// validate the max_backlog, if a queue subscribe ack; set our
// batch size lower if need be so subscribers can make progress.
else if (_ackBatchSize > 1 && ackResponse.options != null &&
ackResponse.options.length() > 0) {
int maxBacklogReturned = extractMaxBacklog(ackResponse.options);
if (maxBacklogReturned > 0 && maxBacklogReturned < _ackBatchSize) {
_ackBatchSize = maxBacklogReturned;
}
}
// all is good, return
return ackResponse;
}
switch (ackResponse.reason) {
case Message.Reason.BadFilter:
throw new BadFilterException("Filter '" + m.getFilter() + "' is invalid.");
case Message.Reason.BadRegexTopic:
throw new BadRegexTopicException("Regular Expression Topic '" + m.getTopic() + "' is invalid.");
case Message.Reason.InvalidTopic:
throw new InvalidTopicException("Topic \"" + m.getTopic() + "\" is invalid.");
case Message.Reason.SubscriptionAlreadyExists:
throw new SubscriptionAlreadyExistsException("Subscription for command '"
+ m.getCommandId() + "' already exists.");
case Message.Reason.SubidInUse:
throw new SubidInUseException("Subscription with subscription id '"
+ m.getSubId() + "' already exists.");
case Message.Reason.NameInUse:
throw new NameInUseException("Client name \"" + m.getClientName() + "\" is already in use.");
case Message.Reason.AuthFailure:
throw new AuthenticationException("Logon failed for user \"" + this.username + "\"");
case Message.Reason.NotEntitled:
if (m.getCommand() == Message.Command.Logon) {
throw new NotEntitledException("User \"" + this.username + "\" not entitled to logon.");
} else {
throw new NotEntitledException(
"User \"" + this.username + "\" not entitled to topic \"" + m.getTopic() + "\".");
}
case Message.Reason.InvalidBookmark:
throw new InvalidBookmarkException("Bookmark \"" + m.getBookmark() + "\" is not valid.");
case Message.Reason.InvalidOrderBy:
throw new InvalidOrderByException("OrderBy \"" + m.getOrderBy() + "\" is not valid.");
case Message.Reason.LogonRequired:
throw new LogonRequiredException();
case Message.Reason.InvalidTopicOrFilter:
throw new InvalidTopicException(
"Invalid topic \"" + m.getTopic() + "\" or filter \"" + m.getFilter() + "\"");
case Message.Reason.InvalidSubId:
throw new InvalidSubIdException("SubId \"" + m.getSubId() + "\" is not valid.");
case Message.Reason.BadSowKey: {
if (m.isSowKeyNull())
throw new BadSowKeyException("Bad sow key \"" + m.getSowKeys() + "\" is not valid.");
else
throw new BadSowKeyException("Bad sow key \"" + m.getSowKey() + "\" is not valid.");
}
case Message.Reason.DuplicateLogon:
throw new DuplicateLogonException();
case Message.Reason.InvalidOptions:
throw new InvalidOptionsException("Options \"" + m.getOptions() + "\" is not valid.");
case Message.Reason.InvalidMessageType:
throw new InvalidMessageTypeException("Invalid or unspecified message type for 'amps' "
+ "protocol connection: message type = " + m.getMessageType());
case Message.Reason.OrderByTooLarge:
throw new InvalidOrderByException("OrderBy \"" + m.getOrderBy() + "\" is too large.");
case Message.Reason.PublishFilterNoMatch:
throw new PublishFilterException(m.getFilter(), m.getData());
default:
throw new CommandException("Error from server: " + ackResponse.reasonText);
}
} else {
// No ack found, must've timed out or disconnected waiting.
if (!ackResponse.abandoned) {
throw new TimedOutException("timed out waiting for operation");
} else {
throw new DisconnectedException("Connection closed while waiting for operation.");
}
}
}
private int extractMaxBacklog(String optionsString) {
// Parse options correctly even though, at the moment, only max_backlog is
// returned.
int optionsLength = optionsString.length();
int keyStart = 0;
int data = 0;
boolean inData = false;
for (int i = 0; i < optionsLength; ++i) {
char c = optionsString.charAt(i);
switch (c) {
case '=':
if (!inData) {
data = 0;
inData = true;
}
break;
case ' ':
if (!inData)
++keyStart;
break;
case ',':
if (optionsString.regionMatches(keyStart, "max_backlog=", 0, 12)) {
return data;
}
data = 0;
keyStart = i + 1;
inData = false;
break;
default:
if (inData) {
data = (data * 10) + ((int) c - 48);
}
}
}
if (optionsString.regionMatches(keyStart, "max_backlog=", 0, 12)) {
return data;
}
return 0;
}
/**
* Return the list of HTTP preflight headers.
*
* @return The list of headers used int he HTTP connect request.
*/
public List getHttpPreflightHeaders() {
return _httpPreflightHeaders;
}
/**
* Add a new header to the list of HTTP preflight headers.
*
* @param header The header string, e.g. Cookie: a=1; b=2
*/
public void addHttpPreflightHeader(String header) {
if (_httpPreflightHeaders == null) {
_httpPreflightHeaders = new ArrayList();
}
_httpPreflightHeaders.add(header);
}
/**
* Add a new key/value pair to the list of HTTP preflight headers.
* It will be added as key: value
*
* @param key The key to add to the list of key/value headers used int he HTTP connect request.
* @param value The value to add to the list of key/value headers used int he HTTP connect request.
*/
public void addHttpPreflightHeader(String key, String value) {
if (_httpPreflightHeaders == null) {
_httpPreflightHeaders = new ArrayList();
}
_httpPreflightHeaders.add(key + ": " + value);
}
/**
* Set the list of HTTP preflight headers replacing any previously set.
*
* @param headers The new list of key/value headers for HTTP connect request.
*/
public void setHttpPreflightHeaders(List headers) {
_httpPreflightHeaders = new ArrayList(headers);
}
/**
* Clear the list of HTTP preflight headers.
*/
public void clearHttpPreflightHeaders() {
if (_httpPreflightHeaders != null) {
_httpPreflightHeaders.clear();
}
}
protected MessageHandler getTransportMessageHandler() {
return _handler;
}
protected TransportDisconnectHandler getTransportDisconnectHandler() {
return _handler;
}
protected class ClientHandler implements MessageHandler, TransportDisconnectHandler {
Client client = null;
CommandId key = new CommandId();
ClientHandler(Client client) {
this.client = client;
}
public void preInvoke(int connectionVersion) {
client.cancelSynchronousWaiters(connectionVersion);
}
/**
* The MessageHandler implementation
*/
public void invoke(Message m) {
final int SOWMask = Message.Command.SOW | Message.Command.GroupBegin | Message.Command.GroupEnd;
final int PublishMask = Message.Command.OOF | Message.Command.Publish | Message.Command.DeltaPublish;
try {
final int commandType = m.getCommand();
if ((commandType & SOWMask) != 0) {
/*
* Not currently implemented to avoid extra branch
* // A small cheat here using knowledge of Message.Commands
* // values for SOW(8), GroupBegin(8192), GroupEnd(16384)
* // and their GlobalCommandTypeHandlers values of 1, 2, 3
* client._globalCommandTypeHandlers[commandType/8192+1]
* .invoke(m);
*/
m.getQueryId(key);
_routes.deliverData(m, key);
} else if ((commandType & PublishMask) != 0) {
/*
* Not currently implemented to avoid extra branch
* client._globalCommandTypeHandlers[
* (commandType==Message.Commands.SOW ?
* GlobalCommandTypeHandlers.SOW :
* GlobalCommandTypeHandler.Publish).invoke(m);
*/
Field subIds = m.getSubIdsRaw();
BookmarkField bookmark = m.getBookmarkRaw();
// Publish messages coming through on a subscription
int index = 0;
while (index < subIds.length) {
int end = index;
for (; end < subIds.length && subIds.buffer[subIds.position + end] != (byte) ','; ++end)
;
key.set(subIds.buffer, subIds.position + index, end - index);
MessageHandler handler = _routes.findRoute(key);
if (handler != null) {
m.setSubId(subIds.buffer, subIds.position + index, end - index);
m.setSubscription(null);
boolean isQueueMessage = m.getLeasePeriodRaw().length > 0;
if (!isQueueMessage && !bookmark.isNull()) {
// send it on to the BookmarkStore for logging,
// only log those messages we're really delivering.
// check to see if the log says its a duplicate
if (bookmarkStore.isDiscarded(m)) {
// yes it is: don't send it
try {
this.client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.DuplicateMessage
.ordinal()].invoke(m);
} catch (Exception e) {
absorbedException(e);
}
} else // not a dupe, log it.
{
bookmarkStore.log(m);
try {
handler.invoke(m);
} catch (Exception e) {
absorbedException(e);
}
}
} else {
boolean thrown = false;
if (isQueueMessage)
m._client = this.client;
try {
handler.invoke(m);
} catch (Exception e) {
thrown = true;
absorbedException(e);
if (isQueueMessage &&
client._isAutoAckEnabled &&
!m.isIgnoreAutoAck()) {
_ack(m.getTopicRaw(), m.getBookmarkRaw(), CANCEL_FIELD);
}
}
if (isQueueMessage && !thrown &&
client._isAutoAckEnabled &&
!m.isIgnoreAutoAck()) {
try {
this.client._ack(m.getTopicRaw(), m.getBookmarkRaw());
} catch (Exception e) {
absorbedException(e);
}
}
} // if !bookmark.isNull()
} // if handler!=null
index = end + 1;
} // while index < subIds.length
} else if (commandType == Message.Command.Ack) {
client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.Ack.ordinal()].invoke(m);
int ackType = m.getAckType();
// call any handlers registered for this ack
_routes.deliverAck(m, ackType);
switch (ackType) {
// Persisted acks are handled internally.
case Message.AckType.Persisted:
persistedAck(m);
return;
// Processed acks are used to respond to waiting synchronous commands.
case Message.AckType.Processed:
processedAck(m);
return;
default:
break;
}
} else if (m.getCommand() == Message.Command.Heartbeat) {
client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.Heartbeat.ordinal()].invoke(m);
if (client.heartbeatTimer.getTimeout() != 0) {
checkAndSendHeartbeat(true);
} else {
client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(m);
}
return;
} else if (_routes.deliverData(m) == 0) {
client._globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(m);
}
checkAndSendHeartbeat(false);
} catch (Exception e) {
this.client.absorbedException(e);
}
}
private void checkAndSendHeartbeat(boolean force) {
if (force || client.heartbeatTimer.check()) {
try {
client.heartbeatTimer.start();
client.sendWithoutRetry(client.beatMessage);
} catch (Exception e) {
absorbedException(e);
}
}
}
private void processedAck(Message message) {
if (message.getCommandId(key)) {
// If this command is awaiting a Processed ack, let's notify it
// that it's arrived
acksLock.lock();
try {
AckResponse response = null;
response = client._acks.remove(key);
if (response != null) {
response.state = message.getStatus();
response.reason = message.getReason();
response.reasonText = message.getReasonText();
response.username = message.getUserId();
response.password = message.getPassword();
response.serverVersion = new VersionInfo(message.getVersion());
response.sequence = message.getSequenceRaw().isNull() ? 0 : message.getSequence();
response.responded = true; // Reset after setting Status/Reason
response.options = message.getOptions();
response.bookmark = message.getBookmark();
ackReceived.signalAll();
return;
}
} catch (Exception e) {
this.client.absorbedException(e);
} finally {
acksLock.unlock();
}
try {
_globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(message);
} catch (Exception e) {
this.client.absorbedException(e);
}
return;
}
}
private void persistedAck(Message message) {
/*
* Best Practice: If you don't care about the dupe acks that occur during
* failover or rapid disconnect/reconnect, then just ignore them.
* We could discard each duplicate from the persisted store, but the
* storage costs of doing 1 record discards is heavy. In most scenarios
* we'll just quickly blow through the duplicates and get back to
* processing the non-dupes.
*/
boolean handled = false;
int reason = message.getReason();
if (!message.isSequenceNull()) {
if (publishStore != null) {
long sequence = ((LongField) message.getSequenceRaw()).getValue();
if (client._failedWriteHandler != null &&
(reason == Message.Reason.Duplicate ||
reason == Message.Reason.NotEntitled ||
message.getStatus() == Message.Status.Failure)) {
try {
FailedWriteStoreReplayer replayer = new FailedWriteStoreReplayer(reason);
publishStore.replaySingle(replayer, sequence);
} catch (Exception e) {
this.client.absorbedException(e);
}
}
handled = true;
try {
publishStore.discardUpTo(sequence);
} catch (Exception e) {
this.client.absorbedException(e);
}
} else if (client._failedWriteHandler != null &&
(reason == Message.Reason.Duplicate ||
reason == Message.Reason.NotEntitled ||
message.getStatus() == Message.Status.Failure)) {
try {
LongField sequenceField = (LongField) message.getSequenceRaw();
long sequence = (sequenceField != null && !sequenceField.isNull())
? sequenceField.getValue()
: 0L;
message.reset();
message.setSequence(sequence);
client._failedWriteHandler.failedWrite(message, reason);
} catch (Exception e) {
this.client.absorbedException(e);
}
handled = true;
}
}
if (!handled && bookmarkStore != null) {
BookmarkField bookmark = message.getBookmarkRaw();
if (bookmark != null && bookmark.length > 1 && !bookmark.isNull()) {
message.getSubId(key);
if (_routes.hasRoute(key)) {
try {
handled = true;
bookmarkStore.persisted(message.getSubIdRaw(), bookmark);
} catch (AMPSException e) {
this.client.absorbedException(e);
}
}
}
}
if (!handled) {
try {
_globalCommandTypeHandlers[GlobalCommandTypeHandlers.LastChance.ordinal()].invoke(message);
} catch (Exception e) {
this.client.absorbedException(e);
}
}
}
// This is our way of "spying" on the activity of the user's disconnect handler.
// We need to know if the user successfully connected to the underlying at least
// once, to tell if they've "given up" on reconnecting or not.
class TransportConnectionMeasurer implements Transport {
Transport _t;
boolean _successfullyConnected = false;
public TransportConnectionMeasurer(Transport underlying) {
_t = underlying;
}
public void clear() {
_successfullyConnected = false;
}
public boolean successfullyConnected() {
return _successfullyConnected;
}
public void connect(URI uri)
throws ConnectionRefusedException, AlreadyConnectedException, InvalidURIException {
_t.connect(uri);
_successfullyConnected = true;
}
public void close() throws Exception {
_t.close();
}
public void disconnect() {
_t.disconnect();
}
public void setMessageHandler(MessageHandler ml) {
_t.setMessageHandler(ml);
}
public void setDisconnectHandler(TransportDisconnectHandler dh) {
_t.setDisconnectHandler(dh);
}
public void setExceptionListener(ExceptionListener exceptionListener) {
_t.setExceptionListener(exceptionListener);
}
public void setThreadCreatedHandler(ThreadCreatedHandler tch_) {
_t.setThreadCreatedHandler(tch_);
}
public void send(Message message) throws DisconnectedException {
_t.send(message);
}
public void sendWithoutRetry(Message message) throws DisconnectedException {
_t.sendWithoutRetry(message);
}
public Message allocateMessage() {
return _t.allocateMessage();
}
public long writeQueueSize() throws DisconnectedException {
return _t.writeQueueSize();
}
public long readQueueSize() throws DisconnectedException {
return _t.readQueueSize();
}
public long flush() throws DisconnectedException {
return _t.flush();
}
public long flush(long timeout) throws DisconnectedException {
return _t.flush(timeout);
}
public void handleCloseEvent(int failedVersion_, String message, Exception e)
throws DisconnectedException, RetryOperationException {
_t.handleCloseEvent(failedVersion_, message, e);
}
public int getVersion() {
return _t.getVersion();
}
public void setReadTimeout(int readTimeoutMillis_) {
_t.setReadTimeout(readTimeoutMillis_);
}
public void setTransportFilter(TransportFilter tracer_) {
_t.setTransportFilter(tracer_);
}
public AMPSRunnable getIdleRunnable() {
return _t.getIdleRunnable();
}
public void setIdleRunnable(AMPSRunnable runnable) {
_t.setIdleRunnable(runnable);
}
public void initFromClient(Client client) {
_t.initFromClient(client);
}
}
/**
* The TransportDisconnectHandler implementation
*/
public void invoke(Transport newTransport, Exception e_) {
Transport oldTransport = null;
try {
lock.lock();
if (this.client.connected)
broadcastConnectionStateChanged(ConnectionStateListener.Disconnected);
oldTransport = transport;
TransportConnectionMeasurer measurer = new TransportConnectionMeasurer(newTransport);
transport = measurer;
while (true) {
boolean connectComplete = false;
try {
// Subscriptions aren't saved by default sub manager
// Only set this to true if we have a real sub manager
_reconnectingSubscriptionManager = !_defaultSubscriptionManager;
measurer.clear();
try {
lock.unlock();
if (disconnectHandler instanceof ClientDisconnectHandler2) {
((ClientDisconnectHandler2) disconnectHandler).invoke(client, e_);
} else {
disconnectHandler.invoke(client);
}
} finally {
lock.lock();
}
connectComplete = true;
} finally {
if (!connectComplete)
_reconnectingSubscriptionManager = false;
_reconnecting.signalAll();
}
// If the disconnect handler successfully connected to
// something, we keep going, even if the connection isn't
// alive now.
if (!measurer.successfullyConnected()) {
broadcastConnectionStateChanged(ConnectionStateListener.Shutdown);
this.client.connected = false;
this.client.absorbedException(new DisconnectedException("reconnect failed."));
_reconnectingSubscriptionManager = false;
_reconnecting.signalAll();
return;
}
try {
lock.unlock();
this.client.subscriptionManager.resubscribe(client);
broadcastConnectionStateChanged(ConnectionStateListener.Resubscribed);
break;
} catch (AMPSException ex) {
// exception attempting to resubscribe, reconnect
this.client.absorbedException(ex);
this.client._disconnect(true);
} finally {
lock.lock();
_reconnectingSubscriptionManager = false;
_reconnecting.signalAll();
}
}
} catch (Exception e) {
broadcastConnectionStateChanged(ConnectionStateListener.Shutdown);
this.client.connected = false;
this.client.absorbedException(e);
} finally {
_reconnectingSubscriptionManager = false;
transport = oldTransport;
lock.unlock();
}
}
}
/**
* Package-access helper method that allows a MessageStream to still
* send heartbeats when it is pushing back on the receive socket
* while the max depth threshold is exceeded.
*/
void checkAndSendHeartbeat(boolean force) {
_handler.checkAndSendHeartbeat(force);
}
/**
* Implementation of {@link StoreReplayer} to replay messages which were
* recorded as published, but did not receive an ack. This is performed as
* part of the logon function by default.
*
*/
static class ClientStoreReplayer implements StoreReplayer {
Client client;
public ClientStoreReplayer(Client c) {
this.client = c;
}
public void execute(Message m) throws DisconnectedException {
if (!m.isCommandNull() &&
(m.isOptionsNull() || !this.client._logonInProgress ||
!m.getOptions().contains("cancel"))) {
m.setAckType(Message.AckType.Persisted | m.getAckTypeOutgoing());
client.sendWithoutRetry(m);
}
long sequence = m.getSequence();
if (sequence > client.lastSentSequenceNumber) {
client.lastSentSequenceNumber = sequence;
}
}
}
private ClientStoreReplayer replayer = null;
static class FailedWriteHandlerV4Compat implements FailedWriteHandler {
FailedWriteHandlerV4 _handler;
public FailedWriteHandlerV4Compat(FailedWriteHandlerV4 handler_) {
_handler = handler_;
}
public void failedWrite(Message m, int reason) {
Field topic = m.getTopicRaw();
Field data = m.getDataRaw();
Field corId = m.getCorrelationIdRaw();
if (m.isDataNull()) {
data = (!m.isFilterNull() ? m.getFilterRaw() : m.getSowKeysRaw());
}
long sequence = m.isSequenceNull() ? 0 : m.getSequence();
_handler.failedWrite(sequence, m.getCommand(),
topic.buffer, topic.position, topic.length,
data.buffer, data.position, data.length,
corId.buffer, corId.position, corId.length,
reason);
}
}
class FailedWriteStoreReplayer implements StoreReplayer {
int _reason;
int _replayCount;
public FailedWriteStoreReplayer(int reason) {
_reason = reason;
_replayCount = 0;
}
public void execute(Message m) {
++_replayCount;
_failedWriteHandler.failedWrite(m, _reason);
}
public int replayCount() {
return _replayCount;
}
}
private void cancelSynchronousWaiters(int connectionVersion) {
ArrayList removeList = new ArrayList();
acksLock.lock();
try {
// _disconnect() calls this with Integer.MAX_VALUE, so don't save that
if (connectionVersion < Integer.MAX_VALUE) {
_lastFailedConnectionVersion = connectionVersion;
}
for (Entry e : _acks.entrySet()) {
// collect items to remove and mark them abandoned.
AckResponse response = e.getValue();
if (response.connectionVersion <= connectionVersion) {
response.abandoned = true;
removeList.add(e.getKey());
}
}
// remove abandoned items from the list
for (CommandId commandId : removeList) {
_acks.remove(commandId);
}
ackReceived.signalAll();
} finally {
acksLock.unlock();
}
}
static private class StopWatch {
private long _timeout;
private long _start;
public StopWatch() {
_timeout = 0;
_start = 0;
}
public boolean check() {
if (_timeout == 0)
return false;
long current = System.currentTimeMillis();
return ((current - _start) > _timeout);
}
public void start() {
_start = System.currentTimeMillis();
}
public void setTimeout(long timeout) {
_start = System.currentTimeMillis();
_timeout = timeout;
}
public long getTimeout() {
return _timeout;
}
}
private class AckFlusherRunnable implements AMPSRunnable {
protected long lastInvokedTime = 0L;
public void run() throws AMPSException {
if (_queueAckTimeout <= 0)
return;
long currentTimeMillis = System.currentTimeMillis();
long elapsed = currentTimeMillis - lastInvokedTime;
// Prevent acquiring the client lock below for potentially every message
// received by requiring that at least 1/16th of _queueAckTimeout (equavalent
// to right shifting it by 4 bits) has elapsed since the last time we
// acquired the client lock. Also allow it through for the very first call,
// when lastInvokedTime == 0.
if (lastInvokedTime > 0L && elapsed < (_queueAckTimeout >> 4)) {
return;
}
lastInvokedTime = currentTimeMillis;
lock.lock();
try {
if (_topicHashMap.size() > 0) {
for (QueueBookmarks q : _topicHashMap.values()) {
if (q._bookmarkCount > 0 && currentTimeMillis - q._oldestTime >= _queueAckTimeout) {
_ack(q);
}
}
}
} finally {
lock.unlock();
}
}
}
}