com.crankuptheamps.client.Client Maven / Gradle / Ivy
////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2010-2021 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.Math;
import java.util.Collection;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import java.util.Map.Entry;
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.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.exception.UnknownException;
import com.crankuptheamps.client.fields.Field;
import com.crankuptheamps.client.fields.BookmarkField;
import com.crankuptheamps.client.fields.CommandField;
import com.crankuptheamps.client.fields.LongField;
import com.crankuptheamps.client.fields.OptionsField;
import com.crankuptheamps.client.fields.StringField;
/**
* The base AMPS client object used in AMPS applications. 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();
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 _reconnectingPublishStore = 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 = 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|";
}
/**
* 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(this._handler);
this.transport.setDisconnectHandler(this._handler);
this.transport.setIdleRunnable(new AckFlusherRunnable());
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();
}
}
/**
* 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 fuction 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 fuction 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 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
{
try
{
this.uri = new URI(uri);
}
catch (URISyntaxException urisex)
{
throw new InvalidURIException(urisex);
}
if(this.transport == null)
{
Properties props = new URIProperties(this.uri);
String uriPath = this.uri.getPath();
String[] sPath = uriPath.substring(1, uriPath.length()).split("/");
String sProtocol = sPath[0];
if(sPath.length > 1)
{
if(!"amps".equals(sProtocol))
{
throw new InvalidURIException("Specification of message type requires amps protocol");
}
}
Protocol messageType = ProtocolFactory.createProtocol(sProtocol, props);
this.transport = TransportFactory
.createTransport(this.uri.getScheme(), 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(this._handler);
this.transport.setDisconnectHandler(this._handler);
this.transport.setIdleRunnable(new AckFlusherRunnable());
}
if (_threadCreatedHandler != null)
{
this.transport.setThreadCreatedHandler(_threadCreatedHandler);
}
if (exceptionListener != null)
{
this.transport.setExceptionListener(exceptionListener);
}
if (_transportFilter != null)
{
this.transport.setTransportFilter(_transportFilter);
}
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 && exceptionListener != 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();
}
}
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)
{
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 && _reconnectingPublishStore)
{
// 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) ||
(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
{
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 occured 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 autmatic 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;
}
StringField subIdField = new StringField();
subIdField.setValue(subid);
if(Client.Bookmarks.MOST_RECENT.equals(bookmark))
{
Field mostRecent = bookmarkStore.getMostRecent(command._message.getSubIdRaw());
command._message.setBookmark(mostRecent.buffer,
mostRecent.position,
mostRecent.length);
}
else if (!Client.Bookmarks.EPOCH.equals(bookmark)
&& !Client.Bookmarks.NOW.equals(bookmark)
&& !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(
command._message.getSubIdRaw(),
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();
// 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.
boolean isRegistered = false;
if(messageHandler!=null && !isSubscribe)
{
if (!_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);
}
else
{
throw new IllegalArgumentException("To use a messagehandler, you must also supply a command or subscription ID.");
}
}
isRegistered = true;
}
final boolean useSyncSend = cid!=null && ((systemAddedAcks|requestedAcks)&Message.AckType.Processed)!=0;
// if there's a command id & a processed ack type set, use syncAckProcessing.
try
{
if(isPublishStore)
{
command._message.setAckType(systemAddedAcks|requestedAcks);
while (true)
{
try {
lock.unlock();
publishStore.store(command._message);
}
finally {
lock.lock();
}
if(useSyncSend)
syncAckProcessing(cid, command._message,
command.getTimeout(),
command._message.getSequence(), false);
else
sendInternal(command._message,
command._message.getSequence());
command.setClientSequenceNumber(command._message.getSequence());
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
{
command._message.setAckType(systemAddedAcks|requestedAcks);
if(useSyncSend) syncAckProcessing(cid,command._message,command.getTimeout()); else sendInternal(command._message);
}
}
}
catch (DisconnectedException 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;
}
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 inverval 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 ublish 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._reconnectingPublishStore = this.publishStore != null;
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)
{
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) // Absorb exceptions
{
this.absorbedException(e);
}
catch (StoreException storeException)
{
throw new ConnectionException("A local store exception occurred while logging on.", storeException);
}
try
{
if(this.publishStore != null)
{
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);
}
return id;
}
finally
{
this._reconnectingPublishStore = 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.
*
* @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
{
lock.lock();
try
{
unsubscribeInternal(subscriptionId);
}
finally
{
lock.unlock();
}
}
private void unsubscribeInternal(CommandId subscriptionId) throws DisconnectedException
{
CommandId id = CommandId.nextIdentifier();
if(subscriptionId != null)
{
lock.unlock();
try
{
this.subscriptionManager.unsubscribe(subscriptionId);
} finally { lock.lock(); }
_routes.removeRoute(subscriptionId);
if (this.transport != null)
{
this.message.reset();
this.message.setCommand(Message.Command.Unsubscribe);
this.message.setCommandId(id);
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 occures, 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 occures, 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;
}
/**
* 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);
}
}
/**
* 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_)
{
_transportFilter = filter_;
if (transport != null) {
if (filter_ != null) {
transport.setTransportFilter(_transportFilter);
}
else {
transport.setTransportFilter(new DefaultTransportFilter());
}
}
}
/**
* Assembles a new ConnectionInfo with the state of this client and
* associated classes.
* @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) return;
_ack(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen);
}
private void _ack(byte[] topic, int topicPos, int topicLen,
byte[] bookmark, int bookmarkPos, int bookmarkLen) throws AMPSException
{
if (bookmark == null || bookmarkLen == 0) 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.getTopicRaw().set(topic,
topicPos,
topicLen);
publishCommand._message.getBookmarkRaw().set(bookmark,
bookmarkPos,
bookmarkLen);
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,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;
if(bookmarksCapacity < bookmarks._length + 1 + bookmarkLen)
{
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();
}
System.arraycopy(bookmark, bookmarkPos, bookmarks._data,
bookmarks._length, bookmarkLen);
bookmarks._length += bookmarkLen;
++bookmarks._bookmarkCount;
if(bookmarks._bookmarkCount >= _ackBatchSize) _ack(bookmarks);
}
finally
{
lock.unlock();
}
}
/**
* 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
{
if (_isAutoAckEnabled) return;
_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;
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);
}
++bookmarks._bookmarkCount;
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;
_ack(topic, topicPos, topicLen, bookmark, bookmarkPos, bookmarkLen,
options, optionsPos, optionsLen);
}
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))
{
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 || !CANCEL_FIELD.equals(publishCommand._message.getOptionsRaw()))
{
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,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;
if(bookmarksCapacity < bookmarks._length + 1 + bookmarkLen)
{
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();
}
System.arraycopy(bookmark, bookmarkPos, bookmarks._data,
bookmarks._length, bookmarkLen);
bookmarks._length += bookmarkLen;
++bookmarks._bookmarkCount;
if(bookmarks._bookmarkCount >= _ackBatchSize) _ack(bookmarks);
}
finally
{
lock.unlock();
}
}
/**
* 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);
}
++bookmarks._bookmarkCount;
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);
}
/**
* 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, where 0 indicates the client will wait until the batch size is full (defaults to 1000)
*/
public void setAckTimeout(long ackTimeout)
{
_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) return ackResponse;
lock.unlock();
try
{
long startTime = System.currentTimeMillis();
long now = startTime;
acksLock.lock();
try
{
while(!ackResponse.abandoned && !ackResponse.responded &&
(timeout == 0 || (now-startTime) < timeout))
{
try
{
if(timeout > 0)
{
long remainingTime = 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 (AMPSException e)
{
absorbedException(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.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;
}
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) ;
Field subId = new Field(subIds.buffer, subIds.position + index, end-index);
key.set(subId.buffer, subId.position, subId.length);
MessageHandler handler = _routes.findRoute(key);
if(handler != null)
{
m.setSubId(subId.buffer, subId.position, subId.length);
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 void setIdleRunnable(AMPSRunnable runnable)
{
_t.setIdleRunnable(runnable);
}
}
/**
* 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
{
// Only set this true if we have a publish store
_reconnectingPublishStore = publishStore != null;
// 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;
_reconnectingPublishStore = 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 (TimedOutException ex)
{
// timed out attempting to resubscribe, pass
this.client.absorbedException(ex);
} catch (DisconnectedException ex)
{
// disconnected exception while attempting to
// resubscribe, pass.
this.client.absorbedException(ex);
}
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._reconnectingPublishStore ||
!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
{
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
{
public void run() throws AMPSException
{
if (_queueAckTimeout <= 0) return;
lock.lock();
try
{
if (_topicHashMap.size() > 0)
{
long currentTimeMillis = System.currentTimeMillis();
for(QueueBookmarks q : _topicHashMap.values())
{
if(q._bookmarkCount > 0 && currentTimeMillis - q._oldestTime >= _queueAckTimeout)
{
_ack(q);
}
}
}
}
finally
{
lock.unlock();
}
}
}
}