com.pushtechnology.diffusion.client.features.TopicUpdate Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2018, 2023 DiffusionData Ltd., All Rights Reserved.
*
* Use is subject to licence terms.
*
* NOTICE: All information contained herein is, and remains the
* property of DiffusionData. The intellectual and technical
* concepts contained herein are proprietary to DiffusionData and
* may be covered by U.S. and Foreign Patents, patents in process, and
* are protected by trade secret or copyright law.
*******************************************************************************/
package com.pushtechnology.diffusion.client.features;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import com.pushtechnology.diffusion.client.features.control.topics.TopicControl.InvalidTopicPathException;
import com.pushtechnology.diffusion.client.features.control.topics.TopicControl.InvalidTopicSpecificationException;
import com.pushtechnology.diffusion.client.features.control.topics.TopicControl.TopicLicenseLimitException;
import com.pushtechnology.diffusion.client.session.Feature;
import com.pushtechnology.diffusion.client.session.PermissionsException;
import com.pushtechnology.diffusion.client.session.Session;
import com.pushtechnology.diffusion.client.session.SessionClosedException;
import com.pushtechnology.diffusion.client.session.SessionException;
import com.pushtechnology.diffusion.client.topics.details.TopicSpecification;
import com.pushtechnology.diffusion.client.topics.details.TopicType;
import com.pushtechnology.diffusion.datatype.Bytes;
import com.pushtechnology.diffusion.datatype.DataTypes;
import com.pushtechnology.diffusion.datatype.binary.Binary;
import com.pushtechnology.diffusion.datatype.json.JSON;
/**
* This feature provides a client session with the ability to update topics.
*
* Topics can be set to new values using stateless
* {@link #set(String, Class, Object) set} operations or an {@link UpdateStream
* UpdateStream}. Both ensure that new values are applied safely to appropriate
* topics.
*
* Additionally, JSON topics can be updated with a {@link #applyJsonPatch JSON
* Patch}. A patch is a list of operations that modifies a JSON value, removing
* the need to supply a complete new value. This is useful if the source of the
* updates doesn't provide values. For one-off, small changes to large JSON
* values, it can be significantly cheaper to apply a patch than to use
* {@code set} to provide the complete value.
*
*
Update streams
*
* An update stream is created for a specific topic. An UpdateStreamBuilder can
* be obtained using {@link UpdateStream.Builder}. The type of the topic must
* match the type of values passed to the update stream. An update stream can be
* used to send any number of updates. It sends a sequence of updates for a
* specific topic to the server. If supported by the data type, updates will be
* sent to the server as a stream of binary deltas.
*
* Update streams have additional ways of failing compared to stateless set
* operations but when used repeatedly have lower overheads. This is because
* update streams maintain a small amount of state that reduces the overhead of
* operations but can become invalid for example, if the topic is deleted, or
* some other session updates the topic value.
*
* By default, update streams use a form of optimistic locking. An update stream
* can update its topic incrementally as long as nothing else updates the topic.
* If the topic is updated independently (for example, by another session, or by
* the current session via set or a different update stream), then the next
* update performed by the update stream will result in an
* {@link InvalidUpdateStreamException}.
*
* Applications can chose to use collaborative locking to coordinate exclusive
* access to a topic. To follow this pattern acquire a
* {@link com.pushtechnology.diffusion.client.session.Session.SessionLock
* session lock}, and use it with a
* {@link UpdateConstraint.Factory#locked(Session.SessionLock) lock constraint}.
* The application is responsible for designing a locking scheme which
* determines which lock is required to access a particular topic, and for
* ensuring that all parts of the application that update the topic follow this
* scheme. Lock constraints and an application locking scheme can also ensure a
* sequence of set operations has exclusive access to the topic.
*
*
Supplying values
*
* When supplying values to an update the value type must be specified
* (valueClass parameter). When using update streams, the value type is
* specified when creating the stream. The class specified will depend upon the
* {@link TopicType} of the topic being updated, according to the supported
* types for the corresponding {@link DataTypes DataType}. For example, for a
* {@link TopicType#JSON JSON} topic, a type of {@link JSON JSON.class} should
* be supplied.
*
* Note that for {@link TopicType#BINARY BINARY} topics the value class can be
* {@link Binary Binary.class} or {@link Bytes Bytes.class} (or any subtype of
* these) but the value must be effectively immutable. This means that any array
* backing the supplied value must not be changed otherwise immutability would
* be violated and results could be unpredictable.
*
*
Removing values
*
* When a {@link TopicType#STRING string}, {@link TopicType#INT64 int64}, or
* {@link TopicType#DOUBLE double} topic is set to {@code null}, the topic will
* be updated to have no value. If a previous value was present subscribers will
* receive a notification that the new value is {@code null}. New subscribers
* will not receive a value notification. Attempting to set any other type of
* topic to {@code null} will cause a {@link NullPointerException} to be thrown.
*
*
Adding topics
*
* When setting a value using either stateless operations or update streams it
* is possible to add a topic if one is not present. This is done using the
* {@link #addAndSet(String, TopicSpecification, Class, Object) addAndSet}
* methods or providing a topic specification when creating the update stream.
* If a topic exists these methods will update the existing topic.
*
*
Time series topics
*
* All methods provided by this feature are compatible with time series topics
* except for {@link #applyJsonPatch(String, String)}. The {@link TimeSeries}
* feature can be used to update time series topics with custom metadata and
* provides query capabilities.
*
*
Access control
*
* To update a topic a session needs
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permission for the topic path. To create a topic a session
* needs
* {@link com.pushtechnology.diffusion.client.types.PathPermission#MODIFY_TOPIC
* MODIFY_TOPIC} permission for the topic path. Requests that combine adding a
* topic and setting the value such as
* {@link #addAndSet(String, TopicSpecification, Class, Object) addAndSet}
* require both permissions.
*
*
Accessing the feature
*
* This feature may be obtained from a {@link Session session} as follows:
*
*
* TopicUpdate topicUpdate = session.feature(TopicUpdate.class);
*
*
* This feature is also extended by the {@link Topics topics feature}. This
* means is it possible to use the methods described here through the
* {@link Topics topics feature}.
*
* @author DiffusionData Limited
* @since 6.2
*/
public interface TopicUpdate extends Feature {
/**
* Sets the topic to a specified value.
*
* The {@code null} value can only be passed to the {@code value} parameter
* when updating {@link TopicType#STRING string}, {@link TopicType#INT64
* int64}, or {@link TopicType#DOUBLE double} topics.
*
* When a {@link TopicType#STRING string}, {@link TopicType#INT64 int64}, or
* {@link TopicType#DOUBLE double} topic is set to {@code null}, the topic
* will be updated to have no value. If a previous value was present
* subscribers will receive a notification that the new value is
* {@code null}. New subscribers will not receive a value notification.
*
* @param path the path of the topic
* @param valueClass the type of the value
* @param value the value. {@link TopicType#STRING String},
* {@link TopicType#INT64 int64}, and {@link TopicType#DOUBLE double}
* topics accept {@code null}, as described above. Using {@code null}
* with other topic types is an error and will throw a
* {@link NullPointerException}.
* @param the type of the value
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If the task fails, the CompletableFuture will complete
* exceptionally with a {@link CompletionException}. Common reasons
* for failure, listed by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link NoSuchTopicException} – if there is no topic
* bound to {@code path};
*
- {@link IncompatibleTopicException} – if updates cannot
* be applied to the topic, for example if a topic view has bound a
* reference topic to the path;
*
- {@link IncompatibleTopicStateException} – if the topic
* is managed by a component (such as fan-out) that prohibits
* updates from the caller;
*
- {@link ClusterRoutingException} – if the operation
* failed due to a transient cluster error;
*
- {@link PermissionsException} – if the calling session
* does not have the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permission for {@code path};
*
- {@link SessionClosedException} – if the session is
* closed.
*
*/
CompletableFuture> set(String path, Class valueClass, T value);
/**
* Sets the topic to a specified value.
*
* Takes a constraint that must be satisfied for the update to be applied.
*
* In other respects this method works in the same way as
* {@link #set(String, Class, Object)}.
*
* @param path the path of the topic
* @param valueClass the type of the value
* @param value the value. {@link TopicType#STRING String},
* {@link TopicType#INT64 int64}, and {@link TopicType#DOUBLE double}
* topics accept {@code null}, as described above. Using {@code null}
* with other topic types is an error and will throw a
* {@link NullPointerException}.
* @param constraint the constraint that must be satisfied for the topic to
* be updated
* @param the type of the value
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If the task fails, the CompletableFuture will complete
* exceptionally with a {@link CompletionException}. Common reasons
* for failure, listed by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link NoSuchTopicException} – if there is no topic
* bound to {@code path};
*
- {@link IncompatibleTopicException} – if updates cannot
* be applied to the topic, for example if a topic view has bound a
* reference topic to the path;
*
- {@link UnsatisfiedConstraintException} – if the
* {@code constraint} is not satisfied by the topic bound to
* {@code path};
*
- {@link IncompatibleTopicStateException} – if the topic
* is managed by a component (such as fan-out) that prohibits
* updates from the caller;
*
- {@link ClusterRoutingException} – if the operation
* failed due to a transient cluster error;
*
- {@link PermissionsException} – if the calling session
* does not have the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permission for {@code path};
*
- {@link SessionClosedException} – if the session is
* closed.
*
*/
CompletableFuture> set(
String path,
Class valueClass,
T value,
UpdateConstraint constraint);
/**
* Ensures a topic exists and sets its value.
*
* If a topic does not exist at the {@code path}, one will be created using
* the {@code specification}. If a topic does exist, its specification must
* match {@code specification}, otherwise the operation will fail with
* {@link IncompatibleTopicException IncompatibleTopicException}.
*
* In other respects this method works in the same way as
* {@link #set(String, Class, Object)}.
*
* @param path the path of the topic
* @param specification the required specification of the topic
* @param valueClass the type of the value
* @param value the value. {@link TopicType#STRING String},
* {@link TopicType#INT64 int64}, and {@link TopicType#DOUBLE double}
* topics accept {@code null}, as described above. Using {@code null}
* with other topic types is an error and will throw a
* {@link NullPointerException}.
* @param the type of the value
* @throws IllegalArgumentException if the type of the specification does
* not match the type of the update
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If the task fails, the CompletableFuture will complete
* exceptionally with a {@link CompletionException}. Common reasons
* for failure, listed by the exception reported as the
* {@link CompletionException#getCause() cause}, include those
* reported by {@link #set(String, Class, Object) set} except
* {@link NoSuchTopicException} as well as:
*
*
* - {@link InvalidTopicPathException} – {@code path} is not
* a valid topic path;
*
- {@link InvalidTopicSpecificationException} – the
* specification is invalid, possibly because mandatory properties
* not supplied;
*
- {@link TopicLicenseLimitException} – the topic could
* not be added as it would breach a licensing limit;
*
- {@link PermissionsException} – if the calling session
* does not have the appropriate permissions for {@code path}; in
* general the calling session must have the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#MODIFY_TOPIC
* MODIFY_TOPIC} and
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permissions, but if the topic exists and has the
* same specification then it's sufficient for the calling session's
* principal to be the {@link TopicSpecification#OWNER topic owner}.
*
*/
CompletableFuture addAndSet(
String path,
TopicSpecification specification,
Class valueClass,
T value);
/**
* Ensures a topic exists and sets its value.
*
* If a topic does not exist at the {@code path}, one will be created using
* the {@code specification}. If a topic does exist, its specification must
* match {@code specification}, otherwise the operation will fail with
* {@link IncompatibleTopicException IncompatibleTopicException}.
*
* Takes a constraint that must be satisfied for the topic to be created or
* the update to be applied.
*
* In other respects this method works in the same way as
* {@link #set(String, Class, Object)}.
*
* @param path the path of the topic
* @param specification the required specification of the topic
* @param valueClass the type of the value
* @param value the value. {@link TopicType#STRING String},
* {@link TopicType#INT64 int64}, and {@link TopicType#DOUBLE double}
* topics accept {@code null}, as described above. Using {@code null}
* with other topic types is an error and will throw a
* {@link NullPointerException}.
* @param constraint the constraint that must be satisfied for the topic to
* be updated
* @param the type of the value
* @throws IllegalArgumentException if the type of the specification does
* not match the type of the update
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If the task fails, the CompletableFuture will complete
* exceptionally with a {@link CompletionException}. Common reasons
* for failure, listed by the exception reported as the
* {@link CompletionException#getCause() cause}, include those
* reported by {@link #set(String, Class, Object, UpdateConstraint)
* set} except {@link NoSuchTopicException} as well as:
*
*
* - {@link InvalidTopicPathException} – {@code path} is not
* a valid topic path;
*
- {@link InvalidTopicSpecificationException} – the
* specification is invalid, possibly because mandatory properties
* not supplied;
*
- {@link TopicLicenseLimitException} – the topic could
* not be added as it would breach a licensing limit;
*
- {@link PermissionsException} – if the calling session
* does not have the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#MODIFY_TOPIC
* MODIFY_TOPIC} and the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permissions for {@code path};
*
*/
CompletableFuture addAndSet(
String path,
TopicSpecification specification,
Class valueClass,
T value,
UpdateConstraint constraint);
/**
* Creates an {@link UpdateStream update stream} to use for updating a
* specific topic.
*
* The type of the topic being updated must match the type derived from the
* {@code valueClass} parameter.
*
* Update streams send a sequence of updates for a specific topic. The
* updates may be delivered to the server as binary deltas. They do not
* provide exclusive access to the topic. If exclusive access is required
* update streams should be used with {@link Session.SessionLock session
* locks} as constraints.
*
* Streams are validated lazily when the first {@link UpdateStream#set} or
* {@link UpdateStream#validate} operation is completed. Once validated a
* stream can be invalidated, after which it rejects future updates.
*
* @param path the path of the topic
* @param valueClass the type of the values expected by the update stream
* @param type of the values expected by the update stream
* @return an update stream
* @deprecated since 6.9
*
* use {@link #newUpdateStreamBuilder}
*/
@Deprecated
UpdateStream createUpdateStream(String path, Class valueClass);
/**
* Creates an {@link UpdateStream update stream} to use for updating a
* specific topic.
*
* Takes a constraint that must be satisfied for the update stream to be
* validated.
*
* In other respects this method works in the same way as
* {@link #createUpdateStream(String, Class)}.
*
* @param path the path of the topic
* @param valueClass the type of the values expected by the update stream
* @param constraint the constraint that must be satisfied for the update
* stream to be validated
* @param type of the values expected by the update stream
* @return an update stream
* @deprecated since 6.9
*
* use {@link #newUpdateStreamBuilder}
*/
@Deprecated
UpdateStream createUpdateStream(
String path, Class valueClass, UpdateConstraint constraint);
/**
* Creates an {@link UpdateStream update stream} to use for creating and
* updating a specific topic.
*
* If a topic does not exist at the {@code path} one will be created using
* the {@code specification} when the update stream is validated. If a topic
* does exist, its specification must match {@code specification}, otherwise
* the operation will fail with {@link IncompatibleTopicException}.
*
* In other respects this method works in the same way as
* {@link #createUpdateStream(String, Class)}.
*
* @param path the path of the topic
* @param specification the required specification of the topic
* @param valueClass the type of the values expected by the update stream
* @param type of the values expected by the update stream
* @throws IllegalArgumentException if the topic type of the specification
* does not match the type of values
* @return an update stream
* @deprecated since 6.9
*
* use {@link #newUpdateStreamBuilder}
*/
@Deprecated
UpdateStream createUpdateStream(
String path, TopicSpecification specification, Class valueClass);
/**
* Creates an {@link UpdateStream update stream} to use for creating and
* updating a specific topic.
*
* If a topic does not exist at the {@code path} one will be created using
* the {@code specification} when the update stream is validated. If a topic
* does exist, its specification must match {@code specification}, otherwise
* the operation will fail with {@link IncompatibleTopicException}.
*
* Takes a constraint that must be satisfied for the update stream to be
* validated.
*
* In other respects this method works in the same way as
* {@link #createUpdateStream(String, Class)}.
*
* @param path the path of the topic
* @param specification the required specification of the topic
* @param valueClass the type of the values expected by the update stream
* @param constraint the constraint that must be satisfied for the update
* stream to be validated
* @param type of the values expected by the update stream
* @throws IllegalArgumentException if the topic type of the specification
* does not match the type of values
* @return an update stream
* @deprecated since 6.9
*
* use {@link #newUpdateStreamBuilder}
*/
@Deprecated
UpdateStream createUpdateStream(
String path,
TopicSpecification specification,
Class valueClass,
UpdateConstraint constraint);
/**
* Creates an update stream builder to use for creating update streams.
*
* @return an update stream builder
* @since 6.9
*/
UpdateStream.Builder newUpdateStreamBuilder();
/**
* Applies a JSON Patch to a JSON topic.
*
*
* The {@code patch} argument should be formatted according to the JSON
* Patch standard (RFC 6902).
*
*
* Patches are a sequence of JSON Patch operations contained in an array.
* They are applied as an atomic update to the previous value if the
* resulting update is successfully calculated. The following patch will
* check the value at a specific key and update if the expected value is
* correct:
*
* [{"op":"test", "path":"/price", "value" : 22}, {"op":"add",
* "path":"/price", "value": 23}]
*
*
* The available operations are:
*
*
* - Add: {@code {"op": "add", "path": "/a/b/c", "value":
* [ "foo", "bar" ]}}
*
- Remove: {@code {"op": "remove", "path": "/a/b/c"}}
*
- Replace: {@code {"op": "replace", "path": "/a/b/c",
* "value": 43}}
*
- Move: {@code {"op": "move", "from": "/a/b/c",
* "path": "/a/b/d"}}
*
- Copy: {@code {"op": "copy", "from": "/a/b/c",
* "path": "/a/b/e"}}
*
- Test: {@code {"op": "test", "path": "/a/b/c",
* "value": "foo"}}
*
*
*
* The test operation checks that the CBOR representation of the value of a
* topic is identical to the value provided in the patch after converting it
* to CBOR. If the value is represented differently as CBOR, commonly due to
* different key ordering, then the patch will return the index of the
* failed operation . e.g the values {@code {"foo": "bar", "count": 43}} and
* {@code {"count": 43, "foo": "bar"}} are unequal despite semantic equality
* due to the differences in a byte for byte comparison.
*
* @param path the path of the topic to patch
* @param patch the JSON Patch
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If the task fails, the CompletableFuture will complete
* exceptionally with a {@link CompletionException}. Common reasons
* for failure, listed by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link InvalidPatchException} – if the patch is not a
* valid JSON Patch;
*
- {@link FailedPatchException} – if applying the patch
* fails, this will occur if the topic's present value is invalid
* CBOR (see {@link TopicSpecification#VALIDATE_VALUES});
*
- {@link NoSuchTopicException} – if there is no topic
* bound to {@code path};
*
- {@link IncompatibleTopicException} – if patch cannot be
* applied to the topic, for example if the topic type is not
* {@link com.pushtechnology.diffusion.datatype.json.JSON}.
*
- {@link IncompatibleTopicStateException} – if the topic
* is managed by a component (such as fan-out) that prohibits
* updates from the caller;
*
- {@link ClusterRoutingException} – if the operation
* failed due to a transient cluster error;
*
- {@link PermissionsException} – if the calling session
* does not have the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permission for {@code path};
*
- {@link SessionClosedException} – if the session is
* closed.
*
*
* @see RFC 6902: JavaScript
* Object Notation (JSON) Patch
*
* @since 6.4
*/
CompletableFuture applyJsonPatch(String path,
String patch);
/**
* Applies a JSON Patch to a JSON topic.
*
*
* Takes a constraint that must be satisfied for the update to be applied.
*
* In other respects this method works in the same way as
* {@link #applyJsonPatch(String, String)}.
*
* @param path the path of the topic to patch
* @param patch the JSON Patch
* @param constraint the constraint that must be satisfied for the patch to
* be applied
* @return a CompletableFuture that completes when a response is received
* from the server.
*
*
* If the task fails, the CompletableFuture will complete
* exceptionally with a {@link CompletionException}. Common reasons
* for failure, listed by the exception reported as the
* {@link CompletionException#getCause() cause}, include:
*
*
* - {@link InvalidPatchException} – if the patch is not a
* valid JSON patch;
*
- {@link FailedPatchException} – if applying the patch
* fails, this will occur if the topic's present value is invalid
* CBOR (see {@link TopicSpecification#VALIDATE_VALUES});
*
- {@link NoSuchTopicException} – if there is no topic
* bound to {@code path};
*
- {@link IncompatibleTopicException} – if patch cannot be
* applied to the topic, for example if the topic type is not
* {@link com.pushtechnology.diffusion.datatype.json.JSON}.
*
- {@link IncompatibleTopicStateException} – if the topic
* is managed by a component (such as fan-out) that prohibits
* updates from the caller;
*
- {@link ClusterRoutingException} – if the operation
* failed due to a transient cluster error;
*
- {@link PermissionsException} – if the calling session
* does not have the
* {@link com.pushtechnology.diffusion.client.types.PathPermission#UPDATE_TOPIC
* UPDATE_TOPIC} permission for {@code path};
*
- {@link SessionClosedException} – if the session is
* closed.
*
*
* @see RFC 6902: JavaScript
* Object Notation (JSON) Patch
*
* @since 6.4
*/
CompletableFuture applyJsonPatch(String path, String patch,
UpdateConstraint constraint);
/**
* Exception thrown to report that a JSON Patch was invalid.
*
* @since 6.4
*/
final class InvalidPatchException extends SessionException {
private static final long serialVersionUID = 2814596011838820640L;
/**
* Constructor.
*/
public InvalidPatchException(String message) {
this(message, null);
}
/**
* Constructor.
*/
public InvalidPatchException(String message, Throwable t) {
super(message, t);
}
}
/**
* Exception thrown to report that applying a JSON Patch failed.
*
*
* This can happen if the topic's current value is not valid CBOR. See
* {@link TopicSpecification#VALIDATE_VALUES}.
*
* @since 6.4
*/
final class FailedPatchException extends SessionException {
private static final long serialVersionUID = 971807945449944880L;
/**
* Constructor.
*/
public FailedPatchException(String message) {
this(message, null);
}
/**
* Constructor.
*/
public FailedPatchException(String message, Throwable t) {
super(message, t);
}
}
/**
* Result of {@link #applyJsonPatch}. Check {@link #failedOperation} to
* determine whether any of the operations failed.
*
* @since 6.4
*/
interface JsonPatchResult {
/**
* @return an {@link Optional} which if present, contains the index of
* the first operation which failed
*/
Optional failedOperation();
}
}