DATAREDIS-864 - Polishing.
Exract nested Stream types into connection.stream package. Extract common code from Template-API based toObjectRecord handling into StreamObjectMapper. StreamReceiver and StreamMessageListenerContainer now can emit MapRecord or ObjectRecord for easier consumption of stream records. Original Pull Request: #356
This commit is contained in:
committed by
Christoph Strobl
parent
13648d7c85
commit
eafcb331d8
@@ -34,6 +34,14 @@ import org.springframework.data.redis.connection.convert.Converters;
|
||||
import org.springframework.data.redis.connection.convert.ListConverter;
|
||||
import org.springframework.data.redis.connection.convert.MapConverter;
|
||||
import org.springframework.data.redis.connection.convert.SetConverter;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StringRecord;
|
||||
import org.springframework.data.redis.core.ConvertingCursor;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
|
||||
@@ -28,6 +28,13 @@ import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Metric;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.redis.connection;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -30,12 +31,12 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -15,30 +15,14 @@
|
||||
*/
|
||||
package org.springframework.data.redis.connection;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.data.redis.connection.stream.*;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Stream-specific Redis commands.
|
||||
@@ -291,736 +275,4 @@ public interface RedisStreamCommands {
|
||||
*/
|
||||
@Nullable
|
||||
Long xTrim(byte[] key, long count);
|
||||
|
||||
/**
|
||||
* Value object representing read offset for a Stream.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
class ReadOffset {
|
||||
|
||||
private final String offset;
|
||||
|
||||
private ReadOffset(String offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the latest offset.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static ReadOffset latest() {
|
||||
return new ReadOffset("$");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all new arriving elements with ids greater than the last one consumed by the consumer group.
|
||||
*
|
||||
* @return the {@link ReadOffset} object without a specific offset.
|
||||
*/
|
||||
public static ReadOffset lastConsumed() {
|
||||
return new ReadOffset(">");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all arriving elements from the stream starting at {@code offset}.
|
||||
*
|
||||
* @param offset the stream offset.
|
||||
* @return the {@link StreamOffset} object without a specific offset.
|
||||
*/
|
||||
public static ReadOffset from(String offset) {
|
||||
|
||||
Assert.hasText(offset, "Offset must not be empty");
|
||||
|
||||
return new ReadOffset(offset);
|
||||
}
|
||||
|
||||
public static ReadOffset from(RecordId offset) {
|
||||
|
||||
if (offset.shouldBeAutoGenerated()) {
|
||||
return latest();
|
||||
}
|
||||
|
||||
return from(offset.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing a Stream Id with its offset.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
class StreamOffset<K> {
|
||||
|
||||
private final K key;
|
||||
private final ReadOffset offset;
|
||||
|
||||
private StreamOffset(K key, ReadOffset offset) {
|
||||
this.key = key;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param readOffset the {@link ReadOffset} to use.
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> create(K key, ReadOffset readOffset) {
|
||||
return new StreamOffset<>(key, readOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
|
||||
*
|
||||
* @param key he stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset latest(K key) {
|
||||
return new StreamOffset(key, ReadOffset.latest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#from(String)
|
||||
* ReadOffset#from("0-0")}.
|
||||
*
|
||||
* @param key he stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset fromStart(K key) {
|
||||
return new StreamOffset(key, ReadOffset.from("0-0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} using the given {@link Record#getId() record id} as reference to create the
|
||||
* {@link ReadOffset#from(String)}.
|
||||
*
|
||||
* @param reference the record to be used as refrence point.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> of(Record<K, ?> reference) {
|
||||
return create(reference.getStream(), ReadOffset.from(reference.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for reading messages from a Redis Stream.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
class StreamReadOptions {
|
||||
|
||||
private static final StreamReadOptions EMPTY = new StreamReadOptions(null, null, false);
|
||||
|
||||
private final @Nullable Long block;
|
||||
private final @Nullable Long count;
|
||||
private final boolean noack;
|
||||
|
||||
private StreamReadOptions(@Nullable Long block, @Nullable Long count, boolean noack) {
|
||||
this.block = block;
|
||||
this.count = count;
|
||||
this.noack = noack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link StreamReadOptions} instance.
|
||||
*
|
||||
* @return an empty {@link StreamReadOptions} instance.
|
||||
*/
|
||||
public static StreamReadOptions empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-acknowledgement when reading in the context of a consumer group.
|
||||
*
|
||||
* @return {@link StreamReadOptions} with {@code noack} applied.
|
||||
*/
|
||||
public StreamReadOptions noack() {
|
||||
return new StreamReadOptions(block, count, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a blocking read and supply the {@link Duration timeout} after which the call will terminate if no message was
|
||||
* read.
|
||||
*
|
||||
* @param timeout the timeout for the blocking read, must not be {@literal null} or negative.
|
||||
* @return {@link StreamReadOptions} with {@code block} applied.
|
||||
*/
|
||||
public StreamReadOptions block(Duration timeout) {
|
||||
|
||||
Assert.notNull(timeout, "Block timeout must not be null!");
|
||||
Assert.isTrue(!timeout.isNegative(), "Block timeout must not be negative!");
|
||||
|
||||
return new StreamReadOptions(timeout.toMillis(), count, noack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of messages returned per stream.
|
||||
*
|
||||
* @param count the maximum number of messages to read.
|
||||
* @return {@link StreamReadOptions} with {@code count} applied.
|
||||
*/
|
||||
public StreamReadOptions count(long count) {
|
||||
|
||||
Assert.isTrue(count > 0, "Count must be greater or equal to zero!");
|
||||
|
||||
return new StreamReadOptions(block, count, noack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing a Stream consumer within a consumer group. Group name and consumer name are encoded as
|
||||
* keys.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@Getter
|
||||
class Consumer {
|
||||
|
||||
private final String group;
|
||||
private final String name;
|
||||
|
||||
private Consumer(String group, String name) {
|
||||
this.group = group;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new consumer.
|
||||
*
|
||||
* @param group name of the consumer group, must not be {@literal null} or empty.
|
||||
* @param name name of the consumer, must not be {@literal null} or empty.
|
||||
* @return the consumer {@link io.lettuce.core.Consumer} object.
|
||||
*/
|
||||
public static Consumer from(String group, String name) {
|
||||
|
||||
Assert.hasText(group, "Group must not be null");
|
||||
Assert.hasText(name, "Name must not be null");
|
||||
|
||||
return new Consumer(group, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s", group, name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of a single {@link Record} within a stream. Composed of two parts:
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
class RecordId {
|
||||
|
||||
private static final String GENERATE_ID = "*";
|
||||
private static final String DELIMINATOR = "-";
|
||||
|
||||
/**
|
||||
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
|
||||
* computation.
|
||||
*/
|
||||
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
|
||||
|
||||
@Override
|
||||
public Long getSequence() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private final String raw;
|
||||
|
||||
/**
|
||||
* Private constructor - validate input in static initializer blocks.
|
||||
*
|
||||
* @param raw
|
||||
*/
|
||||
private RecordId(String raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better,
|
||||
* just use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param value can be {@literal null}.
|
||||
* @return new instance of {@link RecordId} if no autogenerated one requested.
|
||||
*/
|
||||
public static RecordId of(@Nullable String value) {
|
||||
|
||||
if (value == null || GENERATE_ID.equals(value)) {
|
||||
return autoGenerate();
|
||||
}
|
||||
|
||||
Assert.isTrue(value.contains(DELIMINATOR),
|
||||
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
|
||||
return new RecordId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param millisecondsTime
|
||||
* @param sequenceNumber
|
||||
* @return new instance of {@link RecordId}.
|
||||
*/
|
||||
public static RecordId of(long millisecondsTime, long sequenceNumber) {
|
||||
return of(millisecondsTime + DELIMINATOR + sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert
|
||||
* ({@code XADD}).
|
||||
*
|
||||
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
|
||||
*/
|
||||
public static RecordId autoGenerate() {
|
||||
return AUTOGENERATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getTimestamp() {
|
||||
return value(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getSequence() {
|
||||
return value(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
|
||||
*/
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return get the string representation of the {@literal entry-id} in
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>} format or {@literal *} if it
|
||||
* {@link #shouldBeAutoGenerated()}. Never {@literal null}.
|
||||
*/
|
||||
public String getValue() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
private Long value(int index) {
|
||||
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMINATOR)[index], Long.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
|
||||
* collection of {@link MapRecord field/value pairs}).
|
||||
*
|
||||
* @param <V> the type backing the {@link Record}.
|
||||
* @author Christoph Strobl
|
||||
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
|
||||
*/
|
||||
interface Record<S, V> {
|
||||
|
||||
/**
|
||||
* The id of the stream (aka the {@literal key} in Redis).
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
S getStream();
|
||||
|
||||
/**
|
||||
* The id of the entry inside the stream.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
RecordId getId();
|
||||
|
||||
/**
|
||||
* @return the actual content. Never {@literal null}.
|
||||
*/
|
||||
V getValue();
|
||||
|
||||
/**
|
||||
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
|
||||
* <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param map the raw map.
|
||||
* @param <K> the key type of the given {@link Map}.
|
||||
* @param <V> the value type of the given {@link Map}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
|
||||
|
||||
Assert.notNull(map, "Map must not be null!");
|
||||
return StreamRecords.<S, K, V> mapBacked(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
|
||||
* like {@link String} or a complex one. <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param value the value to persist.
|
||||
* @param <V> the type of the backing value.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, V> ObjectRecord<S, V> of(V value) {
|
||||
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
return StreamRecords.objectBacked(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@link RecordId}.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
Record<S, V> withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
|
||||
*
|
||||
* @param key the Redis key identifying the stream.
|
||||
* @param <S1>
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
<S1> Record<S1, V> withStreamKey(S1 key);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or
|
||||
* a complex one.
|
||||
*
|
||||
* @param <V> the type of the backing Object.
|
||||
*/
|
||||
interface ObjectRecord<S, V> extends Record<S, V> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ObjectRecord<S, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
<S1> ObjectRecord<S1, V> withStreamKey(S1 key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <HK> the key type of the resulting {@link MapRecord}.
|
||||
* @param <HV> the value type of the resulting {@link MapRecord}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
|
||||
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
|
||||
*
|
||||
* @param <K> the field type of the backing collection.
|
||||
* @param <V> the value type of the backing collection.
|
||||
*/
|
||||
interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
MapRecord<S, K, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
<S1> MapRecord<S1, K, V> withStreamKey(S1 key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
|
||||
* {@link MapRecord}.
|
||||
*
|
||||
* @param mapFunction must not be {@literal null}.
|
||||
* @param <HK> the field type of the new backing collection.
|
||||
* @param <HV> the value type of the new backing collection.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
|
||||
|
||||
Map<HK, HV> mapped = new LinkedHashMap<>();
|
||||
iterator().forEachRemaining(it -> {
|
||||
|
||||
Entry<HK, HV> mappedPair = mapFunction.apply(it);
|
||||
mapped.put(mappedPair.getKey(), mappedPair.getValue());
|
||||
});
|
||||
|
||||
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
|
||||
}
|
||||
|
||||
default <S1, HK, HV> MapRecord<S1, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<S1, HK, HV>> mapFunction) {
|
||||
return mapFunction.apply(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
default ByteRecord serialize(@Nullable RedisSerializer serializer) {
|
||||
return serialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
|
||||
* id} is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key is binary.
|
||||
* @param fieldSerializer can be {@literal null} if the fields are binary.
|
||||
* @param valueSerializer can be {@literal null} if the values are binary.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
|
||||
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
|
||||
|
||||
MapRecord<S, byte[], byte[]> x = mapEntries(it -> Collections
|
||||
.singletonMap(fieldSerializer != null ? fieldSerializer.serialize(it.getKey()) : (byte[]) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.serialize(it.getValue()) : (byte[]) it.getValue())
|
||||
.entrySet().iterator().next());
|
||||
|
||||
return StreamRecords.newRecord() //
|
||||
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
|
||||
.withId(getId()) //
|
||||
.ofBytes(x.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <OV> type of the value backing the {@link ObjectRecord}.
|
||||
* @return new instance of {@link ObjectRecord}.
|
||||
*/
|
||||
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
|
||||
return Record.<S, OV> of((OV) (mapper).fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteBufferRecord withId(RecordId id);
|
||||
|
||||
ByteBufferRecord withStreamKey(ByteBuffer key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
|
||||
* id} is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections.<HK, HV> singletonMap(
|
||||
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
|
||||
.entrySet().iterator().next())
|
||||
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream()))
|
||||
: (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
// static ByteBufferRecord of(MapRecord<byte[], byte[], byte[]> source) {
|
||||
// return
|
||||
// StreamRecords.newRecord().in(ByteBuffer.wrap(source.getStream())).withId(ByteBuffer.wrap(source.getId())).ofBytes(ByteBuffer.wrap(source.getValue()));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
|
||||
}
|
||||
|
||||
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
|
||||
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
|
||||
|
||||
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
|
||||
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
|
||||
|
||||
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
|
||||
.withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteRecord withId(RecordId id);
|
||||
|
||||
ByteRecord withStreamKey(byte[] key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId
|
||||
* id} is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections
|
||||
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
|
||||
.entrySet().iterator().next())
|
||||
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
interface StringRecord extends MapRecord<String, String, String> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
StringRecord withId(RecordId id);
|
||||
|
||||
StringRecord withStreamKey(String key);
|
||||
|
||||
/**
|
||||
* Turn a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link StringRecord}.
|
||||
*/
|
||||
static StringRecord of(MapRecord<String, String, String> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.geo.Metric;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.StringRecord;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
|
||||
@@ -32,12 +32,11 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyComm
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse;
|
||||
import org.springframework.data.redis.connection.ReactiveStreamCommands;
|
||||
import org.springframework.data.redis.connection.ReactiveStreamCommands.GroupCommand.GroupCommandAction;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -270,7 +269,8 @@ class LettuceReactiveStreamCommands implements ReactiveStreamCommands {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> StreamOffset<T>[] toStreamOffsets(Collection<RedisStreamCommands.StreamOffset<T>> streams) {
|
||||
private static <T> StreamOffset<T>[] toStreamOffsets(
|
||||
Collection<org.springframework.data.redis.connection.stream.StreamOffset<T>> streams) {
|
||||
|
||||
return streams.stream().map(it -> StreamOffset.from(it.getKey(), it.getOffset().getOffset()))
|
||||
.toArray(StreamOffset[]::new);
|
||||
|
||||
@@ -28,8 +28,15 @@ import java.util.function.Function;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,9 +21,9 @@ import io.lettuce.core.XReadArgs;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.connection.convert.ListConverter;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @see 2.2
|
||||
*/
|
||||
public interface ByteBufferRecord extends MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteBufferRecord withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new {@link ByteBufferRecord} with the associated stream {@literal key}.
|
||||
*
|
||||
* @param key the binary stream key.
|
||||
* @return a new {@link ByteBufferRecord}.
|
||||
*/
|
||||
ByteBufferRecord withStreamKey(ByteBuffer key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
|
||||
* is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections.<HK, HV> singletonMap(
|
||||
fieldSerializer != null ? fieldSerializer.deserialize(ByteUtils.getBytes(it.getKey())) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(ByteUtils.getBytes(it.getValue())) : (HV) it.getValue())
|
||||
.entrySet().iterator().next()).withStreamKey(
|
||||
streamSerializer != null ? streamSerializer.deserialize(ByteUtils.getBytes(getStream())) : (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteBufferRecord of(MapRecord<ByteBuffer, ByteBuffer, ByteBuffer> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBuffer(source.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary {@link MapRecord} into an {@link ObjectRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default <OV> ObjectRecord<ByteBuffer, OV> toObjectRecord(
|
||||
HashMapper<? super OV, ? super ByteBuffer, ? super ByteBuffer> mapper) {
|
||||
|
||||
Map<byte[], byte[]> targetMap = getValue().entrySet().stream().collect(
|
||||
Collectors.toMap(entry -> ByteUtils.getBytes(entry.getKey()), entry -> ByteUtils.getBytes(entry.getValue())));
|
||||
|
||||
return Record.<ByteBuffer, OV> of((OV) (mapper).fromHash((Map) targetMap)).withId(getId())
|
||||
.withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of binary {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @see 2.2
|
||||
*/
|
||||
public interface ByteRecord extends MapRecord<byte[], byte[], byte[]> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ByteRecord withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new {@link ByteRecord} with the associated stream {@literal key}.
|
||||
*
|
||||
* @param key the binary stream key.
|
||||
* @return a new {@link ByteRecord}.
|
||||
*/
|
||||
ByteRecord withStreamKey(byte[] key);
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <T> MapRecord<T, T, T> deserialize(@Nullable RedisSerializer<T> serializer) {
|
||||
return deserialize(serializer, serializer, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
|
||||
* is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key suites already the target format.
|
||||
* @param fieldSerializer can be {@literal null} if the fields suite already the target format.
|
||||
* @param valueSerializer can be {@literal null} if the values suite already the target format.
|
||||
* @return new {@link MapRecord} holding the deserialized values.
|
||||
*/
|
||||
default <K, HK, HV> MapRecord<K, HK, HV> deserialize(@Nullable RedisSerializer<? extends K> streamSerializer,
|
||||
@Nullable RedisSerializer<? extends HK> fieldSerializer,
|
||||
@Nullable RedisSerializer<? extends HV> valueSerializer) {
|
||||
|
||||
return mapEntries(it -> Collections
|
||||
.<HK, HV> singletonMap(fieldSerializer != null ? fieldSerializer.deserialize(it.getKey()) : (HK) it.getKey(),
|
||||
valueSerializer != null ? valueSerializer.deserialize(it.getValue()) : (HV) it.getValue())
|
||||
.entrySet().iterator().next())
|
||||
.withStreamKey(streamSerializer != null ? streamSerializer.deserialize(getStream()) : (K) getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary {@link MapRecord} into a {@link ByteRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link ByteRecord}.
|
||||
*/
|
||||
static ByteRecord of(MapRecord<byte[], byte[], byte[]> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofBytes(source.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing a Stream consumer within a consumer group. Group name and consumer name are encoded as
|
||||
* keys.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@Getter
|
||||
public class Consumer {
|
||||
|
||||
private final String group;
|
||||
private final String name;
|
||||
|
||||
private Consumer(String group, String name) {
|
||||
this.group = group;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new consumer.
|
||||
*
|
||||
* @param group name of the consumer group, must not be {@literal null} or empty.
|
||||
* @param name name of the consumer, must not be {@literal null} or empty.
|
||||
* @return the consumer {@link io.lettuce.core.Consumer} object.
|
||||
*/
|
||||
public static Consumer from(String group, String name) {
|
||||
|
||||
Assert.hasText(group, "Group must not be null");
|
||||
Assert.hasText(name, "Name must not be null");
|
||||
|
||||
return new Consumer(group, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s", group, name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords.MapBackedRecord;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@literal field/value} paris.
|
||||
*
|
||||
* @param <K> the field type of the backing map.
|
||||
* @param <V> the value type of the backing map.
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface MapRecord<S, K, V> extends Record<S, Map<K, V>>, Iterable<Map.Entry<K, V>> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link MapRecord} associated with the {@code stream} key and {@link Map value}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param map the value.
|
||||
* @return the {@link ObjectRecord} holding the {@code stream} key and {@code value}.
|
||||
*/
|
||||
static <S, K, V> MapRecord<S, K, V> create(S stream, Map<K, V> map) {
|
||||
|
||||
Assert.notNull(stream, "Stream must not be null");
|
||||
Assert.notNull(map, "Map must not be null");
|
||||
|
||||
return new MapBackedRecord<>(stream, RecordId.autoGenerate(), map);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
MapRecord<S, K, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
<SK> MapRecord<SK, K, V> withStreamKey(SK key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link Function mapFunction} to each and every entry in the backing collection to create a new
|
||||
* {@link MapRecord}.
|
||||
*
|
||||
* @param mapFunction must not be {@literal null}.
|
||||
* @param <HK> the field type of the new backing collection.
|
||||
* @param <HV> the value type of the new backing collection.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> mapEntries(Function<Entry<K, V>, Entry<HK, HV>> mapFunction) {
|
||||
|
||||
Map<HK, HV> mapped = new LinkedHashMap<>();
|
||||
iterator().forEachRemaining(it -> {
|
||||
|
||||
Entry<HK, HV> mappedPair = mapFunction.apply(it);
|
||||
mapped.put(mappedPair.getKey(), mappedPair.getValue());
|
||||
});
|
||||
|
||||
return StreamRecords.newRecord().in(getStream()).withId(getId()).ofMap(mapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map this {@link MapRecord} by applying the mapping {@link Function}.
|
||||
*
|
||||
* @param mapFunction function to apply to this {@link MapRecord} element.
|
||||
* @return the mapped {@link MapRecord}.
|
||||
*/
|
||||
default <SK, HK, HV> MapRecord<SK, HK, HV> map(Function<MapRecord<S, K, V>, MapRecord<SK, HK, HV>> mapFunction) {
|
||||
return mapFunction.apply(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} and {@link #getValue() field/value pairs} with the given
|
||||
* {@link RedisSerializer}. An already assigned {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param serializer can be {@literal null} if the {@link Record} only holds binary data.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default ByteRecord serialize(@Nullable RedisSerializer<?> serializer) {
|
||||
return serialize((RedisSerializer) serializer, (RedisSerializer) serializer, (RedisSerializer) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize {@link #getStream() key} with the {@literal streamSerializer}, field names with the
|
||||
* {@literal fieldSerializer} and values with the {@literal valueSerializer}. An already assigned {@link RecordId id}
|
||||
* is carried over to the new instance.
|
||||
*
|
||||
* @param streamSerializer can be {@literal null} if the key is binary.
|
||||
* @param fieldSerializer can be {@literal null} if the fields are binary.
|
||||
* @param valueSerializer can be {@literal null} if the values are binary.
|
||||
* @return new {@link ByteRecord} holding the serialized values.
|
||||
*/
|
||||
default ByteRecord serialize(@Nullable RedisSerializer<? super S> streamSerializer,
|
||||
@Nullable RedisSerializer<? super K> fieldSerializer, @Nullable RedisSerializer<? super V> valueSerializer) {
|
||||
|
||||
MapRecord<S, byte[], byte[]> binaryMap = mapEntries(
|
||||
it -> Collections.singletonMap(StreamSerialization.serialize(fieldSerializer, it.getKey()),
|
||||
StreamSerialization.serialize(fieldSerializer, it.getValue())).entrySet().iterator().next());
|
||||
|
||||
return StreamRecords.newRecord() //
|
||||
.in(streamSerializer != null ? streamSerializer.serialize(getStream()) : (byte[]) getStream()) //
|
||||
.withId(getId()) //
|
||||
.ofBytes(binaryMap.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <OV> type of the value backing the {@link ObjectRecord}.
|
||||
* @return new instance of {@link ObjectRecord}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
default <OV> ObjectRecord<S, OV> toObjectRecord(HashMapper<? super OV, ? super K, ? super V> mapper) {
|
||||
return Record.<S, OV> of((OV) mapper.fromHash((Map) getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords.ObjectBackedRecord;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream mapped to a single object. This may be a simple type, such as {@link String} or a
|
||||
* complex one.
|
||||
*
|
||||
* @param <V> the type of the backing Object.
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
public interface ObjectRecord<S, V> extends Record<S, V> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link ObjectRecord} associated with the {@code stream} key and {@code value}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param value the value.
|
||||
* @return the {@link ObjectRecord} holding the {@code stream} key and {@code value}.
|
||||
*/
|
||||
static <S, V> ObjectRecord<S, V> create(S stream, V value) {
|
||||
|
||||
Assert.notNull(stream, "Stream must not be null");
|
||||
Assert.notNull(value, "Value must not be null");
|
||||
|
||||
return new ObjectBackedRecord<>(stream, RecordId.autoGenerate(), value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
ObjectRecord<S, V> withId(RecordId id);
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withStreamKey(java.lang.Object)
|
||||
*/
|
||||
<SK> ObjectRecord<SK, V> withStreamKey(SK key);
|
||||
|
||||
/**
|
||||
* Apply the given {@link HashMapper} to the backing value to create a new {@link MapRecord}. An already assigned
|
||||
* {@link RecordId id} is carried over to the new instance.
|
||||
*
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param <HK> the key type of the resulting {@link MapRecord}.
|
||||
* @param <HV> the value type of the resulting {@link MapRecord}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
default <HK, HV> MapRecord<S, HK, HV> toMapRecord(HashMapper<? super V, HK, HV> mapper) {
|
||||
return Record.<S, HK, HV> of(mapper.toHash(getValue())).withId(getId()).withStreamKey(getStream());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing read offset for a Stream.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
public final class ReadOffset {
|
||||
|
||||
private final String offset;
|
||||
|
||||
private ReadOffset(String offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the latest offset.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static ReadOffset latest() {
|
||||
return new ReadOffset("$");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all new arriving elements with ids greater than the last one consumed by the consumer group.
|
||||
*
|
||||
* @return the {@link ReadOffset} object without a specific offset.
|
||||
*/
|
||||
public static ReadOffset lastConsumed() {
|
||||
return new ReadOffset(">");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all arriving elements from the stream starting at {@code offset}.
|
||||
*
|
||||
* @param offset the stream offset.
|
||||
* @return the {@link ReadOffset} starting at {@code offset}.
|
||||
*/
|
||||
public static ReadOffset from(String offset) {
|
||||
|
||||
Assert.hasText(offset, "Offset must not be empty");
|
||||
|
||||
return new ReadOffset(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all arriving elements from the stream starting at {@link RecordId}. Using a
|
||||
* {@link RecordId#shouldBeAutoGenerated() auto-generated} {@link RecordId} returns the {@link #latest()} read offset.
|
||||
*
|
||||
* @param offset the stream offset.
|
||||
* @return the {@link ReadOffset} starting at {@link RecordId}.
|
||||
*/
|
||||
public static ReadOffset from(RecordId offset) {
|
||||
|
||||
if (offset.shouldBeAutoGenerated()) {
|
||||
return latest();
|
||||
}
|
||||
|
||||
return from(offset.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A single entry in the stream consisting of the {@link RecordId entry-id} and the actual entry-value (typically a
|
||||
* collection of {@link MapRecord field/value pairs}).
|
||||
*
|
||||
* @param <V> the type backing the {@link Record}.
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
* @see <a href="https://redis.io/topics/streams-intro#streams-basics">Redis Documentation - Stream Basics</a>
|
||||
*/
|
||||
public interface Record<S, V> {
|
||||
|
||||
/**
|
||||
* The id of the stream (aka the {@literal key} in Redis).
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
S getStream();
|
||||
|
||||
/**
|
||||
* The id of the entry inside the stream.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
RecordId getId();
|
||||
|
||||
/**
|
||||
* @return the actual content. Never {@literal null}.
|
||||
*/
|
||||
V getValue();
|
||||
|
||||
/**
|
||||
* Create a new {@link MapRecord} instance backed by the given {@link Map} holding {@literal field/value} pairs.
|
||||
* <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param map the raw map.
|
||||
* @param <K> the key type of the given {@link Map}.
|
||||
* @param <V> the value type of the given {@link Map}.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, K, V> MapRecord<S, K, V> of(Map<K, V> map) {
|
||||
|
||||
Assert.notNull(map, "Map must not be null!");
|
||||
return StreamRecords.mapBacked(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ObjectRecord} instance backed by the given {@literal value}. The value may be a simple type,
|
||||
* like {@link String} or a complex one. <br />
|
||||
* You may want to use the builders available via {@link StreamRecords}.
|
||||
*
|
||||
* @param value the value to persist.
|
||||
* @param <V> the type of the backing value.
|
||||
* @return new instance of {@link MapRecord}.
|
||||
*/
|
||||
static <S, V> ObjectRecord<S, V> of(V value) {
|
||||
|
||||
Assert.notNull(value, "Value must not be null!");
|
||||
return StreamRecords.objectBacked(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@link RecordId}.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
Record<S, V> withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link Record} with the given {@literal key} to store the record at.
|
||||
*
|
||||
* @param key the Redis key identifying the stream.
|
||||
* @param <SK>
|
||||
* @return new instance of {@link Record}.
|
||||
*/
|
||||
<SK> Record<SK, V> withStreamKey(SK key);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The id of a single {@link Record} within a stream. Composed of two parts:
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
* @see <a href="https://redis.io/topics/streams-intro#entry-ids">Redis Documentation - Entriy ID</a>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
public class RecordId {
|
||||
|
||||
private static final String GENERATE_ID = "*";
|
||||
private static final String DELIMITER = "-";
|
||||
|
||||
/**
|
||||
* Auto-generation of IDs by the server is almost always what you want so we've got this instance here shortcutting
|
||||
* computation.
|
||||
*/
|
||||
private static final RecordId AUTOGENERATED = new RecordId(GENERATE_ID) {
|
||||
|
||||
@Override
|
||||
public Long getSequence() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private final String raw;
|
||||
|
||||
/**
|
||||
* Private constructor - validate input in static initializer blocks.
|
||||
*
|
||||
* @param raw
|
||||
*/
|
||||
private RecordId(String raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain an instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert pass in {@literal null} or {@literal *}. Event better, just
|
||||
* use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param value can be {@literal null}.
|
||||
* @return new instance of {@link RecordId} if no autogenerated one requested.
|
||||
*/
|
||||
public static RecordId of(@Nullable String value) {
|
||||
|
||||
if (value == null || GENERATE_ID.equals(value)) {
|
||||
return autoGenerate();
|
||||
}
|
||||
|
||||
Assert.isTrue(value.contains(DELIMITER),
|
||||
"Invalid id format. Please use the 'millisecondsTime-sequenceNumber' format.");
|
||||
return new RecordId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link RecordId} using the provided String formatted as
|
||||
* {@literal <millisecondsTime>-<sequenceNumber>}. <br />
|
||||
* For server auto generated {@literal entry-id} on insert use {@link #autoGenerate()}.
|
||||
*
|
||||
* @param millisecondsTime
|
||||
* @param sequenceNumber
|
||||
* @return new instance of {@link RecordId}.
|
||||
*/
|
||||
public static RecordId of(long millisecondsTime, long sequenceNumber) {
|
||||
return of(millisecondsTime + DELIMITER + sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the {@link RecordId} signalling the server to auto generate an {@literal entry-id} on insert ({@code XADD}).
|
||||
*
|
||||
* @return {@link RecordId} instance signalling {@link #shouldBeAutoGenerated()}.
|
||||
*/
|
||||
public static RecordId autoGenerate() {
|
||||
return AUTOGENERATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id millisecondsTime} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return millisecondsTime of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getTimestamp() {
|
||||
return value(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@literal entry-id sequenceNumber} part or {@literal null} if it {@link #shouldBeAutoGenerated()}.
|
||||
*
|
||||
* @return sequenceNumber of the {@literal entry-id}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Long getSequence() {
|
||||
return value(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a new {@literal entry-id} shall be generated on server side when calling {@code XADD}.
|
||||
*/
|
||||
public boolean shouldBeAutoGenerated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return get the string representation of the {@literal entry-id} in {@literal <millisecondsTime>-<sequenceNumber>}
|
||||
* format or {@literal *} if it {@link #shouldBeAutoGenerated()}. Never {@literal null}.
|
||||
*/
|
||||
public String getValue() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
private Long value(int index) {
|
||||
return NumberUtils.parseNumber(StringUtils.split(raw, DELIMITER)[index], Long.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing a Stream Id with its offset.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
public final class StreamOffset<K> {
|
||||
|
||||
private final K key;
|
||||
private final ReadOffset offset;
|
||||
|
||||
private StreamOffset(K key, ReadOffset offset) {
|
||||
|
||||
Assert.notNull(key, "Key must not be null");
|
||||
Assert.notNull(offset, "ReadOffset must not be null");
|
||||
|
||||
this.key = key;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} and {@link ReadOffset}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param readOffset the {@link ReadOffset} to use.
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> create(K stream, ReadOffset readOffset) {
|
||||
return new StreamOffset<>(stream, readOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code key} starting at {@link ReadOffset#latest()}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> latest(K stream) {
|
||||
return new StreamOffset<>(stream, ReadOffset.latest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} given {@code stream} starting at {@link ReadOffset#from(String)
|
||||
* ReadOffset#from("0-0")}.
|
||||
*
|
||||
* @param stream the stream key.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> fromStart(K stream) {
|
||||
return new StreamOffset<>(stream, ReadOffset.from("0-0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StreamOffset} from the given {@link Record#getId() record id} as reference to create the
|
||||
* {@link ReadOffset#from(String)}.
|
||||
*
|
||||
* @param reference the record to be used as reference point.
|
||||
* @param <K>
|
||||
* @return new instance of {@link StreamOffset}.
|
||||
*/
|
||||
public static <K> StreamOffset<K> from(Record<K, ?> reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference record must not be null");
|
||||
|
||||
return create(reference.getStream(), ReadOffset.from(reference.getId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Options for reading messages from a Redis Stream.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
public class StreamReadOptions {
|
||||
|
||||
private static final StreamReadOptions EMPTY = new StreamReadOptions(null, null, false);
|
||||
|
||||
private final @Nullable Long block;
|
||||
private final @Nullable Long count;
|
||||
private final boolean noack;
|
||||
|
||||
private StreamReadOptions(@Nullable Long block, @Nullable Long count, boolean noack) {
|
||||
this.block = block;
|
||||
this.count = count;
|
||||
this.noack = noack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty {@link StreamReadOptions} instance.
|
||||
*
|
||||
* @return an empty {@link StreamReadOptions} instance.
|
||||
*/
|
||||
public static StreamReadOptions empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-acknowledgement when reading in the context of a consumer group.
|
||||
*
|
||||
* @return {@link StreamReadOptions} with {@code noack} applied.
|
||||
*/
|
||||
public StreamReadOptions noack() {
|
||||
return new StreamReadOptions(block, count, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a blocking read and supply the {@link Duration timeout} after which the call will terminate if no message was
|
||||
* read.
|
||||
*
|
||||
* @param timeout the timeout for the blocking read, must not be {@literal null} or negative.
|
||||
* @return {@link StreamReadOptions} with {@code block} applied.
|
||||
*/
|
||||
public StreamReadOptions block(Duration timeout) {
|
||||
|
||||
Assert.notNull(timeout, "Block timeout must not be null!");
|
||||
Assert.isTrue(!timeout.isNegative(), "Block timeout must not be negative!");
|
||||
|
||||
return new StreamReadOptions(timeout.toMillis(), count, noack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the number of messages returned per stream.
|
||||
*
|
||||
* @param count the maximum number of messages to read.
|
||||
* @return {@link StreamReadOptions} with {@code count} applied.
|
||||
*/
|
||||
public StreamReadOptions count(long count) {
|
||||
|
||||
Assert.isTrue(count > 0, "Count must be greater or equal to zero!");
|
||||
|
||||
return new StreamReadOptions(block, count, noack);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,19 @@
|
||||
package org.springframework.data.redis.connection;
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@@ -7,20 +22,14 @@ import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StringRecord;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link StreamRecords} provides utilities to create specific
|
||||
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record} instances.
|
||||
* {@link StreamRecords} provides utilities to create specific {@link Record} instances.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
@@ -83,15 +92,22 @@ public class StreamRecords {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain new instance of {@link RecordBuilder} to fluently create
|
||||
* {@link org.springframework.data.redis.connection.RedisStreamCommands.Record records}.
|
||||
* Obtain new instance of {@link RecordBuilder} to fluently create {@link Record records}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static RecordBuilder newRecord() {
|
||||
return new RecordBuilder(null, RecordId.autoGenerate());
|
||||
public static RecordBuilder<?> newRecord() {
|
||||
return new RecordBuilder<>(null, RecordId.autoGenerate());
|
||||
}
|
||||
|
||||
// Utility constructor
|
||||
private StreamRecords() {}
|
||||
|
||||
/**
|
||||
* Builder for {@link Record}.
|
||||
*
|
||||
* @param <S> stream keyy type.
|
||||
*/
|
||||
public static class RecordBuilder<S> {
|
||||
|
||||
private RecordId id;
|
||||
@@ -103,21 +119,49 @@ public class StreamRecords {
|
||||
this.id = recordId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a stream key.
|
||||
*
|
||||
* @param stream the stream key, must not be null.
|
||||
* @param <STREAM_KEY>
|
||||
* @return {@literal this} {@link RecordBuilder}.
|
||||
*/
|
||||
public <STREAM_KEY> RecordBuilder<STREAM_KEY> in(STREAM_KEY stream) {
|
||||
|
||||
Assert.notNull(stream, "Stream key must not be null");
|
||||
|
||||
return new RecordBuilder<>(stream, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a record Id given a {@link String}. Associates a user-supplied record id instead of using
|
||||
* server-generated record Id's.
|
||||
*
|
||||
* @param id the record id.
|
||||
* @return {@literal this} {@link RecordBuilder}.
|
||||
* @see RecordId
|
||||
*/
|
||||
public RecordBuilder<S> withId(String id) {
|
||||
return withId(RecordId.of(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link RecordId}. Associates a user-supplied record id instead of using server-generated record Id's.
|
||||
*
|
||||
* @param id the record id.
|
||||
* @return {@literal this} {@link RecordBuilder}.
|
||||
*/
|
||||
public RecordBuilder<S> withId(RecordId id) {
|
||||
|
||||
Assert.notNull(id, "RecordId must not be null");
|
||||
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link MapRecord}.
|
||||
*
|
||||
* @param map
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
@@ -128,17 +172,22 @@ public class StreamRecords {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StringRecord}.
|
||||
*
|
||||
* @param map
|
||||
* @return new instance of {@link StringRecord}.
|
||||
* @see MapRecord
|
||||
*/
|
||||
public StringRecord ofStrings(Map<String, String> map) {
|
||||
return new StringMapBackedRecord(ObjectUtils.nullSafeToString(stream), id, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link ObjectRecord}.
|
||||
*
|
||||
* @param value
|
||||
* @param <V>
|
||||
* @return ni instance of {@link ObjectRecord}.
|
||||
* @return new instance of {@link ObjectRecord}.
|
||||
*/
|
||||
public <V> ObjectRecord<S, V> ofObject(V value) {
|
||||
return new ObjectBackedRecord<>(stream, id, value);
|
||||
@@ -160,7 +209,7 @@ public class StreamRecords {
|
||||
*/
|
||||
public ByteBufferRecord ofBuffer(Map<ByteBuffer, ByteBuffer> value) {
|
||||
|
||||
ByteBuffer streamKey = null;
|
||||
ByteBuffer streamKey;
|
||||
|
||||
if (stream instanceof ByteBuffer) {
|
||||
streamKey = (ByteBuffer) stream;
|
||||
@@ -176,13 +225,20 @@ public class StreamRecords {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link MapRecord}.
|
||||
*
|
||||
* @param <S>
|
||||
* @param <K>
|
||||
* @param <V>
|
||||
*/
|
||||
static class MapBackedRecord<S, K, V> implements MapRecord<S, K, V> {
|
||||
|
||||
private @Nullable S stream;
|
||||
private RecordId recordId;
|
||||
private final Map<K, V> kvMap;
|
||||
|
||||
MapBackedRecord(S stream, RecordId recordId, Map<K, V> kvMap) {
|
||||
MapBackedRecord(@Nullable S stream, RecordId recordId, Map<K, V> kvMap) {
|
||||
|
||||
this.stream = stream;
|
||||
this.recordId = recordId;
|
||||
@@ -263,6 +319,9 @@ public class StreamRecords {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ByteRecord}.
|
||||
*/
|
||||
static class ByteMapBackedRecord extends MapBackedRecord<byte[], byte[], byte[]> implements ByteRecord {
|
||||
|
||||
ByteMapBackedRecord(byte[] stream, RecordId recordId, Map<byte[], byte[]> map) {
|
||||
@@ -274,11 +333,15 @@ public class StreamRecords {
|
||||
return new ByteMapBackedRecord(key, getId(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteMapBackedRecord withId(RecordId id) {
|
||||
return new ByteMapBackedRecord(getStream(), id, getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ByteBufferRecord}.
|
||||
*/
|
||||
static class ByteBufferMapBackedRecord extends MapBackedRecord<ByteBuffer, ByteBuffer, ByteBuffer>
|
||||
implements ByteBufferRecord {
|
||||
|
||||
@@ -291,11 +354,15 @@ public class StreamRecords {
|
||||
return new ByteBufferMapBackedRecord(key, getId(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufferMapBackedRecord withId(RecordId id) {
|
||||
return new ByteBufferMapBackedRecord(getStream(), id, getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of StringRecord.
|
||||
*/
|
||||
static class StringMapBackedRecord extends MapBackedRecord<String, String, String> implements StringRecord {
|
||||
|
||||
StringMapBackedRecord(String stream, RecordId recordId, Map<String, String> stringStringMap) {
|
||||
@@ -307,11 +374,18 @@ public class StreamRecords {
|
||||
return new StringMapBackedRecord(key, getId(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringMapBackedRecord withId(RecordId id) {
|
||||
return new StringMapBackedRecord(getStream(), id, getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ObjectRecord}.
|
||||
*
|
||||
* @param <S>
|
||||
* @param <V>
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
static class ObjectBackedRecord<S, V> implements ObjectRecord<S, V> {
|
||||
|
||||
@@ -319,7 +393,7 @@ public class StreamRecords {
|
||||
private RecordId recordId;
|
||||
private final V value;
|
||||
|
||||
public ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
|
||||
ObjectBackedRecord(@Nullable S stream, RecordId recordId, V value) {
|
||||
|
||||
this.stream = stream;
|
||||
this.recordId = recordId;
|
||||
@@ -349,7 +423,7 @@ public class StreamRecords {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S1> ObjectRecord<S1, V> withStreamKey(S1 key) {
|
||||
public <SK> ObjectRecord<SK, V> withStreamKey(SK key) {
|
||||
return new ObjectBackedRecord<>(key, recordId, value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Utility methods for stream serialization.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
class StreamSerialization {
|
||||
|
||||
/**
|
||||
* Serialize the {@code value} using the optional {@link RedisSerializer}. If no conversion is possible, {@code value}
|
||||
* is assumed to be a byte array.
|
||||
*
|
||||
* @param serializer the serializer. Can be {@literal null}.
|
||||
* @param value the value to serialize.
|
||||
* @return the serialized (binary) representation of {@code value}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static byte[] serialize(@Nullable RedisSerializer<?> serializer, Object value) {
|
||||
return canSerialize(serializer, value) ? ((RedisSerializer) serializer).serialize(value) : (byte[]) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link RedisSerializer} is capable of serializing the {@code value} to {@literal byte[]}.
|
||||
*
|
||||
* @param serializer the serializer. Can be {@literal null}.
|
||||
* @param value the value to serialize.
|
||||
* @return {@literal true} if the given {@link RedisSerializer} is capable of serializing the {@code value} to
|
||||
* {@literal byte[]}.
|
||||
*/
|
||||
private static boolean canSerialize(@Nullable RedisSerializer<?> serializer, Object value) {
|
||||
return serializer != null && (value == null || serializer.canSerialize(value.getClass()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
|
||||
/**
|
||||
* A {@link Record} within the stream backed by a collection of {@link String} {@literal field/value} paris.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface StringRecord extends MapRecord<String, String, String> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.RedisStreamCommands.Record#withId(org.springframework.data.redis.connection.RedisStreamCommands.RecordId)
|
||||
*/
|
||||
@Override
|
||||
StringRecord withId(RecordId id);
|
||||
|
||||
/**
|
||||
* Create a new {@link StringRecord} with the associated stream {@literal key}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @return a new {@link StringRecord}.
|
||||
*/
|
||||
StringRecord withStreamKey(String key);
|
||||
|
||||
/**
|
||||
* Convert a {@link MapRecord} of {@link String strings} into a {@link StringRecord}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @return new instance of {@link StringRecord}.
|
||||
*/
|
||||
static StringRecord of(MapRecord<String, String, String> source) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofStrings(source.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Data structures and interfaces to interact with Redis Streams.
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
@org.springframework.lang.NonNullFields
|
||||
package org.springframework.data.redis.connection.stream;
|
||||
@@ -19,11 +19,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
@@ -25,23 +25,21 @@ import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.ReactiveStreamCommands;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.connection.stream.ByteBufferRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -58,20 +56,60 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
|
||||
private final ReactiveRedisTemplate<?, ?> template;
|
||||
private final RedisSerializationContext<K, ?> serializationContext;
|
||||
private final StreamObjectMapper objectMapper;
|
||||
|
||||
private final RedisCustomConversions rcc = new RedisCustomConversions();
|
||||
private DefaultConversionService conversionService;
|
||||
private HashMapper<? super K, ? super HK, ? super HV> mapper;
|
||||
|
||||
public DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
|
||||
DefaultReactiveStreamOperations(ReactiveRedisTemplate<?, ?> template,
|
||||
RedisSerializationContext<K, ?> serializationContext,
|
||||
@Nullable HashMapper<? super K, ? super HK, ? super HV> hashMapper) {
|
||||
|
||||
this.template = template;
|
||||
this.serializationContext = serializationContext;
|
||||
this.objectMapper = new StreamObjectMapper(hashMapper) {
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
this.mapper = mapper != null ? mapper : (HashMapper<? super K, ? super HK, ? super HV>) new ObjectHashMapper();
|
||||
rcc.registerConvertersIn(conversionService);
|
||||
@Override
|
||||
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
|
||||
|
||||
if (objectMapper.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
|
||||
|
||||
return new HashMapper<Object, Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toHash(Object object) {
|
||||
|
||||
Object key = "payload";
|
||||
Object value = object;
|
||||
|
||||
if (serializationContext.getHashKeySerializationPair() == null) {
|
||||
key = key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (serializationContext.getHashValueSerializationPair() == null) {
|
||||
value = conversionService.convert(value, byte[].class);
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromHash(Map<Object, Object> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
HV deserialized = deserializeHashValue((ByteBuffer) value);
|
||||
|
||||
if (ClassUtils.isAssignableValue(targetType, deserialized)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return conversionService.convert(deserialized, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return super.doGetHashMapper(conversionService, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -94,12 +132,14 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
* @see org.springframework.data.redis.core.ReactiveStreamOperations#add(java.lang.Object, java.util.Map)
|
||||
*/
|
||||
@Override
|
||||
public Mono<RecordId> add(MapRecord<K, HK, HV> record) {
|
||||
public Mono<RecordId> add(Record<K, ?> record) {
|
||||
|
||||
Assert.notNull(record.getStream(), "Key must not be null!");
|
||||
Assert.notEmpty(record.getValue(), "Body must not be null!");
|
||||
Assert.notNull(record.getValue(), "Body must not be null!");
|
||||
|
||||
return createMono(connection -> connection.xAdd(serializeRecord(record)));
|
||||
MapRecord<K, HK, HV> input = StreamObjectMapper.toMapRecord(this, record);
|
||||
|
||||
return createMono(connection -> connection.xAdd(serializeRecord(input)));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -234,64 +274,7 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
|
||||
@Override
|
||||
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
|
||||
if (rcc.isSimpleType(targetType) || ClassUtils.isAssignable(ByteBuffer.class, targetType)) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
|
||||
HK key = (HK) "payload";
|
||||
HV value = (HV) object;
|
||||
|
||||
if (serializationContext.getHashKeySerializationPair() == null) {
|
||||
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (serializationContext.getHashValueSerializationPair() == null) {
|
||||
value = (HV) conversionService.convert(value, byte[].class);
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
|
||||
// return (Map<HK, HV>) Collections.singletonMap("payload".getBytes(StandardCharsets.UTF_8),
|
||||
// serializeHashValueIfRequires((HV) object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return (V) value;
|
||||
}
|
||||
return (V) deserializeHashValue((ByteBuffer) value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
|
||||
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
|
||||
|
||||
return (V) mapper.fromHash((Map) map);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return (HashMapper<V, HK, HV>) mapper;
|
||||
return objectMapper.getHashMapper(targetType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -319,21 +302,24 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
return serializationContext.getKeySerializationPair().write(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ByteBuffer rawHashKey(HK key) {
|
||||
try {
|
||||
return serializationContext.getHashKeySerializationPair().write(key);
|
||||
} catch (IllegalStateException e) {}
|
||||
return ByteBuffer.wrap(conversionService.convert(key, byte[].class));
|
||||
return ByteBuffer.wrap(objectMapper.getConversionService().convert(key, byte[].class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ByteBuffer rawValue(HV value) {
|
||||
|
||||
try {
|
||||
return serializationContext.getHashValueSerializationPair().write(value);
|
||||
} catch (IllegalStateException e) {}
|
||||
return ByteBuffer.wrap(conversionService.convert(value, byte[].class));
|
||||
return ByteBuffer.wrap(objectMapper.getConversionService().convert(value, byte[].class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private HK readHashKey(ByteBuffer buffer) {
|
||||
return (HK) serializationContext.getHashKeySerializationPair().getReader().read(buffer);
|
||||
}
|
||||
@@ -342,6 +328,7 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
return serializationContext.getKeySerializationPair().read(buffer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private HV deserializeHashValue(ByteBuffer buffer) {
|
||||
return (HV) serializationContext.getHashValueSerializationPair().read(buffer);
|
||||
}
|
||||
@@ -355,12 +342,12 @@ class DefaultReactiveStreamOperations<K, HK, HV> implements ReactiveStreamOperat
|
||||
.next();
|
||||
}
|
||||
|
||||
private ByteBufferRecord serializeRecord(MapRecord<K, HK, HV> record) {
|
||||
private ByteBufferRecord serializeRecord(MapRecord<K, ? extends HK, ? extends HV> record) {
|
||||
return ByteBufferRecord
|
||||
.of(record.map(it -> it.mapEntries(this::serializeRecordFields).withStreamKey(rawKey(record.getStream()))));
|
||||
}
|
||||
|
||||
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<HK, HV> it) {
|
||||
private Entry<ByteBuffer, ByteBuffer> serializeRecordFields(Entry<? extends HK, ? extends HV> it) {
|
||||
return Collections.singletonMap(rawHashKey(it.getKey()), rawValue(it.getValue())).entrySet().iterator().next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,24 +21,23 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ByteRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.connection.stream.ByteRecord;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -50,17 +49,61 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> implements StreamOperations<K, HK, HV> {
|
||||
|
||||
private final RedisCustomConversions rcc = new RedisCustomConversions();
|
||||
private DefaultConversionService conversionService;
|
||||
private HashMapper<?, ? super HK, ? super HV> mapper;
|
||||
private final StreamObjectMapper objectMapper;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
DefaultStreamOperations(RedisTemplate<K, ?> template,
|
||||
@Nullable HashMapper<? super K, ? super HK, ? super HV> mapper) {
|
||||
|
||||
super((RedisTemplate<K, Object>) template);
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
this.mapper = mapper != null ? mapper : (HashMapper<?, HK, HV>) new ObjectHashMapper();
|
||||
rcc.registerConvertersIn(conversionService);
|
||||
this.objectMapper = new StreamObjectMapper(mapper) {
|
||||
@Override
|
||||
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
|
||||
|
||||
if (isSimpleType(targetType)) {
|
||||
|
||||
return new HashMapper<Object, Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toHash(Object object) {
|
||||
|
||||
Object key = "payload";
|
||||
Object value = object;
|
||||
|
||||
if (!template.isEnableDefaultSerializer()) {
|
||||
if (template.getHashKeySerializer() == null) {
|
||||
key = key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (template.getHashValueSerializer() == null) {
|
||||
value = serializeHashValueIfRequires((HV) object);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromHash(Map<Object, Object> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
HV deserialized = deserializeHashValue((byte[]) value);
|
||||
|
||||
if (ClassUtils.isAssignableValue(targetType, deserialized)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return conversionService.convert(deserialized, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return super.doGetHashMapper(conversionService, targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -76,12 +119,17 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.StreamOperations#add(java.lang.Object, java.util.Map)
|
||||
* @see org.springframework.data.redis.core.StreamOperations#add(org.springframework.data.redis.connection.stream.Record)
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public RecordId add(MapRecord<K, HK, HV> record) {
|
||||
public RecordId add(Record<K, ?> record) {
|
||||
|
||||
ByteRecord binaryRecord = record.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
|
||||
Assert.notNull(record, "Record must not be null");
|
||||
|
||||
MapRecord<K, HK, HV> input = StreamObjectMapper.toMapRecord(this, record);
|
||||
|
||||
ByteRecord binaryRecord = input.serialize(keySerializer(), hashKeySerializer(), hashValueSerializer());
|
||||
|
||||
return execute(connection -> connection.xAdd(binaryRecord), true);
|
||||
}
|
||||
@@ -148,7 +196,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -165,7 +213,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -182,7 +230,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -199,7 +247,7 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
@Override
|
||||
public List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit) {
|
||||
|
||||
return execute(new RecordDeserializingRedisCallback<K, HK, HV>() {
|
||||
return execute(new RecordDeserializingRedisCallback() {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -218,68 +266,12 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
|
||||
@Override
|
||||
public <V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
|
||||
if (rcc.isSimpleType(targetType)) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
|
||||
HK key = (HK) "payload";
|
||||
HV value = (HV) object;
|
||||
|
||||
if (!template.isEnableDefaultSerializer()) {
|
||||
if (template.getHashKeySerializer() == null) {
|
||||
key = (HK) key.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
if (template.getHashValueSerializer() == null) {
|
||||
value = (HV) serializeHashValueIfRequires((HV) object);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.singletonMap(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
Object value = hash.values().iterator().next();
|
||||
if (ClassUtils.isAssignableValue(targetType, value)) {
|
||||
return (V) value;
|
||||
}
|
||||
return (V) deserializeHashValue((byte[]) value, (Class<HV>) targetType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
return new HashMapper<V, HK, HV>() {
|
||||
|
||||
@Override
|
||||
public Map<HK, HV> toHash(V object) {
|
||||
return (Map<HK, HV>) ((ObjectHashMapper) mapper).toObjectHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V fromHash(Map<HK, HV> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert((Object) e.getKey(), byte[].class),
|
||||
e -> conversionService.convert((Object) e.getValue(), byte[].class)));
|
||||
|
||||
return (V) mapper.fromHash((Map) map);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return (HashMapper<V, HK, HV>) mapper;
|
||||
return objectMapper.getHashMapper(targetType);
|
||||
}
|
||||
|
||||
protected byte[] serializeHashValueIfRequires(HV value) {
|
||||
return hashValueSerializerPresent() ? serialize(value, hashValueSerializer())
|
||||
: conversionService.convert(value, byte[].class);
|
||||
: objectMapper.getConversionService().convert(value, byte[].class);
|
||||
}
|
||||
|
||||
protected boolean hashValueSerializerPresent() {
|
||||
@@ -288,14 +280,14 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
|
||||
protected HV deserializeHashValue(byte[] bytes, Class<HV> targetType) {
|
||||
return hashValueSerializerPresent() ? (HV) hashValueSerializer().deserialize(bytes)
|
||||
: conversionService.convert(bytes, targetType);
|
||||
: objectMapper.getConversionService().convert(bytes, targetType);
|
||||
}
|
||||
|
||||
byte[] serialize(Object value, RedisSerializer serializer) {
|
||||
private byte[] serialize(Object value, RedisSerializer serializer) {
|
||||
|
||||
Object _value = value;
|
||||
if (!serializer.canSerialize(value.getClass())) {
|
||||
_value = conversionService.convert(value, serializer.getTargetType());
|
||||
_value = objectMapper.getConversionService().convert(value, serializer.getTargetType());
|
||||
}
|
||||
return serializer.serialize(_value);
|
||||
}
|
||||
@@ -308,14 +300,14 @@ class DefaultStreamOperations<K, HK, HV> extends AbstractOperations<K, Object> i
|
||||
.toArray(it -> new StreamOffset[it]);
|
||||
}
|
||||
|
||||
abstract class RecordDeserializingRedisCallback<K, HK, HV> implements RedisCallback<List<MapRecord<K, HK, HV>>> {
|
||||
abstract class RecordDeserializingRedisCallback implements RedisCallback<List<MapRecord<K, HK, HV>>> {
|
||||
|
||||
public final List<MapRecord<K, HK, HV>> doInRedis(RedisConnection connection) {
|
||||
|
||||
List<ByteRecord> x = inRedis(connection);
|
||||
List<ByteRecord> raw = inRedis(connection);
|
||||
|
||||
List<MapRecord<K, HK, HV>> result = new ArrayList<>();
|
||||
for (ByteRecord record : x) {
|
||||
for (ByteRecord record : raw) {
|
||||
result.add(record.deserialize(keySerializer(), hashKeySerializer(), hashValueSerializer()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.core;
|
||||
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
|
||||
/**
|
||||
* Function that returns a {@link HashMapper} for a given {@link Class type}.
|
||||
* <p/>
|
||||
* Implementors of this interface can return a generic or a specific {@link HashMapper} implementation, depending on the
|
||||
* serialization strategy for the requested {@link Class target type}.
|
||||
*
|
||||
* @param <HK>
|
||||
* @param <HV>
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface HashMapperProvider<HK, HV> {
|
||||
|
||||
/**
|
||||
* Get the {@link HashMapper} for a specific type.
|
||||
*
|
||||
* @param targetType must not be {@literal null}.
|
||||
* @param <V> the value target type.
|
||||
* @return the {@link HashMapper} suitable for a given type;
|
||||
*/
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.redis.core;
|
||||
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -645,9 +646,10 @@ public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForStream(org.springframework.data.redis.serializer.RedisSerializationContext)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
|
||||
RedisSerializationContext<K, ?> serializationContext) {
|
||||
return opsForStream(serializationContext, null);
|
||||
return opsForStream(serializationContext, (HashMapper) new ObjectHashMapper());
|
||||
}
|
||||
|
||||
protected <HK, HV> ReactiveStreamOperations<K, HK, HV> opsForStream(
|
||||
|
||||
@@ -23,17 +23,18 @@ import java.util.Map;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Redis stream specific operations.
|
||||
@@ -42,7 +43,7 @@ import org.springframework.data.redis.hash.HashMapper;
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
public interface ReactiveStreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV> {
|
||||
|
||||
/**
|
||||
* Acknowledge one or more records as processed.
|
||||
@@ -88,7 +89,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @return the record Ids.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
default Flux<RecordId> add(K key, Publisher<? extends Map<HK, HV>> bodyPublisher) {
|
||||
default Flux<RecordId> add(K key, Publisher<? extends Map<? extends HK, ? extends HV>> bodyPublisher) {
|
||||
return Flux.from(bodyPublisher).flatMap(it -> add(key, it));
|
||||
}
|
||||
|
||||
@@ -96,12 +97,12 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* Append a record to the stream {@code key}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param body record body.
|
||||
* @param content record content as Map.
|
||||
* @return the {@link Mono} emitting the {@link RecordId}.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
default Mono<RecordId> add(K key, Map<HK, HV> body) {
|
||||
return add(StreamRecords.newRecord().in(key).ofMap(body));
|
||||
default Mono<RecordId> add(K key, Map<? extends HK, ? extends HV> content) {
|
||||
return add(StreamRecords.newRecord().in(key).ofMap(content));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,18 +112,20 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @return the {@link Mono} emitting the {@link RecordId}.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
Mono<RecordId> add(MapRecord<K, HK, HV> record);
|
||||
@SuppressWarnings("unchecked")
|
||||
default Mono<RecordId> add(MapRecord<K, ? extends HK, ? extends HV> record) {
|
||||
return add((Record) record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
|
||||
*
|
||||
* @param record must not be {@literal null}.
|
||||
* @param <V>
|
||||
* @return
|
||||
* @return the {@link Mono} emitting the {@link RecordId}.
|
||||
* @see MapRecord
|
||||
* @see ObjectRecord
|
||||
*/
|
||||
default <V> Mono<RecordId> add(Record<K, V> record) {
|
||||
return add(toMapRecord(record));
|
||||
}
|
||||
Mono<RecordId> add(Record<K, ?> record);
|
||||
|
||||
/**
|
||||
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
|
||||
@@ -163,7 +166,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key
|
||||
* @param group name of the consumer group.
|
||||
* @return the {@link Mono} emitting {@literal ok} if successful.. {@literal null} when used in pipeline /
|
||||
* @return the {@link Mono} emitting {@literal OK} if successful.. {@literal null} when used in pipeline /
|
||||
* transaction.
|
||||
*/
|
||||
default Mono<String> createGroup(K key, String group) {
|
||||
@@ -176,7 +179,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @param key
|
||||
* @param readOffset
|
||||
* @param group name of the consumer group.
|
||||
* @return the {@link Mono} emitting {@literal ok} if successful.
|
||||
* @return the {@link Mono} emitting {@literal OK} if successful.
|
||||
*/
|
||||
Mono<String> createGroup(K key, ReadOffset readOffset, String group);
|
||||
|
||||
@@ -185,7 +188,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param consumer consumer identified by group name and consumer key.
|
||||
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return the {@link Mono} {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
Mono<String> deleteConsumer(K key, Consumer consumer);
|
||||
|
||||
@@ -194,7 +197,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param group name of the consumer group.
|
||||
* @return the {@link Mono} {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return the {@link Mono} {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
Mono<String> destroyGroup(K key, String group);
|
||||
|
||||
@@ -212,95 +215,131 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return the {@link Flux} emitting the records one by one.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range) {
|
||||
return range(key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all records from a stream within a specific {@link Range}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return lthe {@link Flux} emitting the records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
|
||||
return range(key, range, Limit.unlimited(), targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return lthe {@link Flux} emitting the records one by one.
|
||||
* @return lthe {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
|
||||
|
||||
/**
|
||||
* Read all records from a stream within a specific {@link Range}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range) {
|
||||
return range(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return lthe {@link Flux} emitting the records one by one.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> range(K key, Range<String> range, Limit limit, Class<V> targetType) {
|
||||
return range(key, range, limit).map(it -> toObjectRecord(it, targetType));
|
||||
default <V> Flux<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return range(key, range, limit).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a {@link StreamOffset} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param stream the stream to read from.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
|
||||
|
||||
Assert.notNull(stream, "StreamOffset must not be null");
|
||||
|
||||
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a {@link StreamOffset} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param stream the stream to read from.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K> stream) {
|
||||
|
||||
Assert.notNull(stream, "StreamOffset must not be null");
|
||||
|
||||
return read(targetType, StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param targetType
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
|
||||
return read(StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param streams the streams to read from.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
|
||||
return read(targetType, StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(StreamOffset<K>... streams) {
|
||||
return read(StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @oaram targetType
|
||||
* @param targetType the target type of the payload.
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
|
||||
return read(readOptions, streams).map(it -> toObjectRecord(it, targetType));
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return read(readOptions, streams).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,7 +347,7 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param consumer consumer/group.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K>... streams) {
|
||||
@@ -316,11 +355,12 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamOffset<K>... streams) {
|
||||
@@ -333,24 +373,27 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @param consumer consumer/group.
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
* @param readOptions read arguments.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
return read(consumer, readOptions, streams).map(it -> toObjectRecord(it, targetType));
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return read(consumer, readOptions, streams).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,40 +401,53 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range) {
|
||||
return reverseRange(key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
|
||||
return reverseRange(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order.
|
||||
* Read records from a stream within a specific {@link Range} in reverse order as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
|
||||
return reverseRange(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order as
|
||||
* {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return list with members of the resulting stream.
|
||||
* @return the {@link Flux} emitting records one by one.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default <V> Flux<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
return reverseRange(key, range, limit).map(it -> toObjectRecord(it, targetType));
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return reverseRange(key, range, limit).map(it -> StreamObjectMapper.toObjectRecord(this, it, targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,37 +467,6 @@ public interface ReactiveStreamOperations<K, HK, HV> {
|
||||
* @param <V>
|
||||
* @return the {@link HashMapper} suitable for a given type;
|
||||
*/
|
||||
@Override
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
|
||||
/**
|
||||
* App
|
||||
*
|
||||
* @param value
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
|
||||
|
||||
if (value instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) value);
|
||||
|
||||
// TODO: should we have this?
|
||||
if (entry.getValue() instanceof Map) {
|
||||
return StreamRecords.newRecord().in(value.getStream()).withId(value.getId()).ofMap((Map) entry.getValue());
|
||||
}
|
||||
|
||||
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
|
||||
}
|
||||
|
||||
if (value instanceof MapRecord) {
|
||||
return (MapRecord<K, HK, HV>) value;
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
|
||||
}
|
||||
|
||||
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
|
||||
return entry.toObjectRecord(getHashMapper(targetType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.core.query.SortQuery;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.core.types.RedisClientInfo;
|
||||
@@ -627,8 +628,7 @@ public interface RedisOperations<K, V> {
|
||||
/**
|
||||
* Returns the operations performed on Streams.
|
||||
*
|
||||
* @param hashMapper the {@link HashMapper} to use when converting
|
||||
* {@link org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord}.
|
||||
* @param hashMapper the {@link HashMapper} to use when converting {@link ObjectRecord}.
|
||||
* @return stream operations.
|
||||
* @since 2.2
|
||||
*/
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.data.redis.core.script.ScriptExecutor;
|
||||
import org.springframework.data.redis.core.types.RedisClientInfo;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationUtils;
|
||||
@@ -1310,7 +1311,7 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
|
||||
public <HK, HV> StreamOperations<K, HK, HV> opsForStream() {
|
||||
|
||||
if (streamOps == null) {
|
||||
streamOps = new DefaultStreamOperations<>(this, null);
|
||||
streamOps = new DefaultStreamOperations<>(this, new ObjectHashMapper());
|
||||
}
|
||||
return (StreamOperations<K, HK, HV>) streamOps;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.core.convert.RedisCustomConversions;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility to provide a {@link HashMapper} for Stream object conversion.
|
||||
* <p/>
|
||||
* This utility can use generic a {@link HashMapper} or adapt specifically to {@link ObjectHashMapper}'s requirement to
|
||||
* convert incoming data into byte arrays. This class can be subclassed to override template methods for specific object
|
||||
* mapping strategies.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
* @see ObjectHashMapper
|
||||
* @see #doGetHashMapper(ConversionService, Class)
|
||||
*/
|
||||
class StreamObjectMapper {
|
||||
|
||||
private final DefaultConversionService conversionService = new DefaultConversionService();
|
||||
private final RedisCustomConversions customConversions = new RedisCustomConversions();
|
||||
private final HashMapper<Object, Object, Object> mapper;
|
||||
private final @Nullable HashMapper<Object, Object, Object> objectHashMapper;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StreamObjectMapper}.
|
||||
*
|
||||
* @param mapper the configured {@link HashMapper}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
StreamObjectMapper(HashMapper<?, ?, ?> mapper) {
|
||||
|
||||
Assert.notNull(mapper, "HashMapper must not be null");
|
||||
|
||||
this.customConversions.registerConvertersIn(conversionService);
|
||||
this.mapper = (HashMapper) mapper;
|
||||
|
||||
if (mapper instanceof ObjectHashMapper) {
|
||||
|
||||
ObjectHashMapper ohm = (ObjectHashMapper) mapper;
|
||||
this.objectHashMapper = new HashMapper<Object, Object, Object>() {
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toHash(Object object) {
|
||||
return (Map) ohm.toHash(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromHash(Map<Object, Object> hash) {
|
||||
|
||||
Map<byte[], byte[]> map = hash.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> conversionService.convert(e.getKey(), byte[].class),
|
||||
e -> conversionService.convert(e.getValue(), byte[].class)));
|
||||
|
||||
return ohm.fromHash(map);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.objectHashMapper = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@link Record} into a {@link MapRecord}.
|
||||
*
|
||||
* @param provider provider for {@link HashMapper} to apply mapping for {@link ObjectRecord}.
|
||||
* @param source the source value.
|
||||
* @return the converted {@link MapRecord}.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
static <K, V, HK, HV> MapRecord<K, HK, HV> toMapRecord(HashMapperProvider<HK, HV> provider, Record<K, V> source) {
|
||||
|
||||
if (source instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) source);
|
||||
|
||||
if (entry.getValue() instanceof Map) {
|
||||
return StreamRecords.newRecord().in(source.getStream()).withId(source.getId()).ofMap((Map) entry.getValue());
|
||||
}
|
||||
|
||||
return entry.toMapRecord(provider.getHashMapper(entry.getValue().getClass()));
|
||||
}
|
||||
|
||||
if (source instanceof MapRecord) {
|
||||
return (MapRecord<K, HK, HV>) source;
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) provider.getHashMapper(source.getClass())).toHash(source))
|
||||
.withStreamKey(source.getStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@link Record} into an {@link ObjectRecord}.
|
||||
*
|
||||
* @param provider provider for {@link HashMapper} to apply mapping for {@link ObjectRecord}.
|
||||
* @param source the source value.
|
||||
* @param targetType the desired target type.
|
||||
* @return the converted {@link ObjectRecord}.
|
||||
*/
|
||||
static <K, V, HK, HV> ObjectRecord<K, V> toObjectRecord(HashMapperProvider<HK, HV> provider,
|
||||
MapRecord<K, HK, HV> source, Class<V> targetType) {
|
||||
return source.toObjectRecord(provider.getHashMapper(targetType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a {@link List} of {@link MapRecord}s to a {@link List} of {@link ObjectRecord}. Optimizes for empty,
|
||||
* single-element and multi-element list transformation.l
|
||||
*
|
||||
* @param records the {@link MapRecord} that should be mapped.
|
||||
* @param hashMapperProvider
|
||||
* @param targetType the requested {@link Class target type}.
|
||||
* @return the resulting {@link List} of {@link ObjectRecord} or {@literal null} if {@code records} was
|
||||
* {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
static <K, V, HK, HV> List<ObjectRecord<K, V>> map(@Nullable List<MapRecord<K, HK, HV>> records,
|
||||
HashMapperProvider<HK, HV> hashMapperProvider, Class<V> targetType) {
|
||||
|
||||
if (records == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (records.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (records.size() == 1) {
|
||||
return Collections.singletonList(toObjectRecord(hashMapperProvider, records.get(0), targetType));
|
||||
}
|
||||
|
||||
List<ObjectRecord<K, V>> transformed = new ArrayList<>(records.size());
|
||||
HashMapper<V, HK, HV> hashMapper = hashMapperProvider.getHashMapper(targetType);
|
||||
|
||||
for (MapRecord<K, HK, HV> record : records) {
|
||||
transformed.add(record.toObjectRecord(hashMapper));
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final <V, HK, HV> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
|
||||
return (HashMapper) doGetHashMapper(conversionService, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual {@link HashMapper}. Can be overridden by subclasses.
|
||||
*
|
||||
* @param conversionService the used {@link ConversionService}.
|
||||
* @param targetType the target type.
|
||||
* @return
|
||||
*/
|
||||
protected HashMapper<?, ?, ?> doGetHashMapper(ConversionService conversionService, Class<?> targetType) {
|
||||
return this.objectHashMapper != null ? objectHashMapper : this.mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetType
|
||||
* @return {@literal true} if {@link Class targetType} is a simple type.
|
||||
* @see org.springframework.data.convert.CustomConversions#isSimpleType(Class)
|
||||
*/
|
||||
boolean isSimpleType(Class<?> targetType) {
|
||||
return customConversions.isSimpleType(targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return used {@link ConversionService}.
|
||||
*/
|
||||
ConversionService getConversionService() {
|
||||
return this.conversionService;
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,21 @@ import reactor.core.publisher.Mono;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.RecordId;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
|
||||
import org.springframework.data.redis.connection.StreamRecords;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Redis stream specific operations.
|
||||
@@ -43,7 +43,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Christoph Strobl
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface StreamOperations<K, HK, HV> {
|
||||
public interface StreamOperations<K, HK, HV> extends HashMapperProvider<HK, HV> {
|
||||
|
||||
/**
|
||||
* Acknowledge one or more records as processed.
|
||||
@@ -91,8 +91,9 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @return the record Id. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
default RecordId add(K key, Map<HK, HV> content) {
|
||||
default RecordId add(K key, Map<? extends HK, ? extends HV> content) {
|
||||
return add(StreamRecords.newRecord().in(key).ofMap(content));
|
||||
}
|
||||
|
||||
@@ -104,25 +105,29 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @see <a href="http://redis.io/commands/xadd">Redis Documentation: XADD</a>
|
||||
*/
|
||||
@Nullable
|
||||
RecordId add(MapRecord<K, HK, HV> record);
|
||||
|
||||
/**
|
||||
* Append the record, backed by the given value, to the stream. The value will be hashed and serialized.
|
||||
*
|
||||
* @param record must not be {@literal null}.
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
default <V> RecordId add(Record<K, V> record) {
|
||||
return add(toMapRecord(record));
|
||||
@SuppressWarnings("unchecked")
|
||||
default RecordId add(MapRecord<K, ? extends HK, ? extends HV> record) {
|
||||
return add((Record) record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified entries from the stream. Returns the number of items deleted, that may be different from the
|
||||
* number of IDs passed in case certain IDs do not exist.
|
||||
* Append the record, backed by the given value, to the stream. The value is mapped as hash and serialized.
|
||||
*
|
||||
* @param record must not be {@literal null}.
|
||||
* @return the record Id. {@literal null} when used in pipeline / transaction.
|
||||
* @see MapRecord
|
||||
* @see ObjectRecord
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
RecordId add(Record<K, ?> record);
|
||||
|
||||
/**
|
||||
* Removes the specified records from the stream. Returns the number of records deleted, that may be different from
|
||||
* the number of IDs passed in case certain IDs do not exist.
|
||||
*
|
||||
* @param key the stream key.
|
||||
* @param recordIds stream record id's.
|
||||
* @param recordIds stream record Id's.
|
||||
* @return number of removed entries. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xdel">Redis Documentation: XDEL</a>
|
||||
*/
|
||||
@@ -159,7 +164,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
*
|
||||
* @param key
|
||||
* @param group name of the consumer group.
|
||||
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
default String createGroup(K key, String group) {
|
||||
return createGroup(key, ReadOffset.latest(), group);
|
||||
@@ -171,7 +176,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @param key
|
||||
* @param readOffset
|
||||
* @param group name of the consumer group.
|
||||
* @return {@literal ok} if successful. {@literal null} when used in pipeline / transaction.
|
||||
* @return {@literal OK} if successful. {@literal null} when used in pipeline / transaction.
|
||||
*/
|
||||
@Nullable
|
||||
String createGroup(K key, ReadOffset readOffset, String group);
|
||||
@@ -231,31 +236,34 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
List<MapRecord<K, HK, HV>> range(K key, Range<String> range, Limit limit);
|
||||
|
||||
default <V> List<ObjectRecord<K, V>> range(K key, Range<String> range, Class<V> targetType) {
|
||||
return range(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
|
||||
/**
|
||||
* Read all records from a stream within a specific {@link Range} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range) {
|
||||
return range(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} as {@link ObjectRecord}.
|
||||
*
|
||||
* @param stream the streams to read from.
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
* @see <a href="http://redis.io/commands/xrange">Redis Documentation: XRANGE</a>
|
||||
*/
|
||||
@Nullable
|
||||
default List<MapRecord<K, HK, HV>> read(StreamOffset<K> stream) {
|
||||
return read(StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
}
|
||||
default <V> List<ObjectRecord<K, V>> range(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
|
||||
return read(targetType, StreamReadOptions.empty(), streams);
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(range(key, range, limit), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,16 +279,15 @@ public interface StreamOperations<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
*
|
||||
* @param readOptions read arguments.
|
||||
* @param stream the streams to read from.
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param streams the streams to read from.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xread">Redis Documentation: XREAD</a>
|
||||
*/
|
||||
@Nullable
|
||||
default List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K> stream) {
|
||||
return read(readOptions, new StreamOffset[] { stream });
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamOffset<K>... streams) {
|
||||
return read(targetType, StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +302,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
List<MapRecord<K, HK, HV>> read(StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s.
|
||||
* Read records from one or more {@link StreamOffset}s as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param readOptions read arguments.
|
||||
@@ -306,20 +313,10 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
return read(readOptions, streams).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
*
|
||||
* @param consumer consumer/group.
|
||||
* @param stream the streams to read from.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
@Nullable
|
||||
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamOffset<K> stream) {
|
||||
return read(consumer, StreamReadOptions.empty(), new StreamOffset[] { stream });
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(read(readOptions, streams), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,7 +333,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
@@ -349,20 +346,6 @@ public interface StreamOperations<K, HK, HV> {
|
||||
return read(targetType, consumer, StreamReadOptions.empty(), streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
*
|
||||
* @param consumer consumer/group.
|
||||
* @param readOptions read arguments.
|
||||
* @param stream the streams to read from.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xreadgroup">Redis Documentation: XREADGROUP</a>
|
||||
*/
|
||||
@Nullable
|
||||
default List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K> stream) {
|
||||
return read(consumer, readOptions, new StreamOffset[] { stream });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
*
|
||||
@@ -376,7 +359,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
List<MapRecord<K, HK, HV>> read(Consumer consumer, StreamReadOptions readOptions, StreamOffset<K>... streams);
|
||||
|
||||
/**
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group.
|
||||
* Read records from one or more {@link StreamOffset}s using a consumer group as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param consumer consumer/group.
|
||||
@@ -388,8 +371,10 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
default <V> List<ObjectRecord<K, V>> read(Class<V> targetType, Consumer consumer, StreamReadOptions readOptions,
|
||||
StreamOffset<K>... streams) {
|
||||
return read(consumer, readOptions, streams).stream().map(it -> toObjectRecord(it, targetType))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(read(consumer, readOptions, streams), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,8 +402,35 @@ public interface StreamOperations<K, HK, HV> {
|
||||
@Nullable
|
||||
List<MapRecord<K, HK, HV>> reverseRange(K key, Range<String> range, Limit limit);
|
||||
|
||||
default <V> List<ObjectRecord<K, V>> reverseRange(K key, Range<String> range, Class<V> targetType) {
|
||||
return reverseRange(key, range).stream().map(it -> toObjectRecord(it, targetType)).collect(Collectors.toList());
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} in reverse order as {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range) {
|
||||
return reverseRange(targetType, key, range, Limit.unlimited());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read records from a stream within a specific {@link Range} applying a {@link Limit} in reverse order as
|
||||
* {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType the target type of the payload.
|
||||
* @param key the stream key.
|
||||
* @param range must not be {@literal null}.
|
||||
* @param limit must not be {@literal null}.
|
||||
* @return list with members of the resulting stream. {@literal null} when used in pipeline / transaction.
|
||||
* @see <a href="http://redis.io/commands/xrevrange">Redis Documentation: XREVRANGE</a>
|
||||
*/
|
||||
default <V> List<ObjectRecord<K, V>> reverseRange(Class<V> targetType, K key, Range<String> range, Limit limit) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
return StreamObjectMapper.map(reverseRange(key, range, limit), this, targetType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,31 +451,7 @@ public interface StreamOperations<K, HK, HV> {
|
||||
* @param <V>
|
||||
* @return the {@link HashMapper} suitable for a given type;
|
||||
*/
|
||||
@Override
|
||||
<V> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType);
|
||||
|
||||
/**
|
||||
* App
|
||||
*
|
||||
* @param value
|
||||
* @param <V>
|
||||
* @return
|
||||
*/
|
||||
default <V> MapRecord<K, HK, HV> toMapRecord(Record<K, V> value) {
|
||||
|
||||
if (value instanceof ObjectRecord) {
|
||||
|
||||
ObjectRecord entry = ((ObjectRecord) value);
|
||||
return entry.toMapRecord(getHashMapper(entry.getValue().getClass()));
|
||||
}
|
||||
|
||||
if (value instanceof MapRecord) {
|
||||
return (MapRecord<K, HK, HV>) value;
|
||||
}
|
||||
|
||||
return Record.of(((HashMapper) getHashMapper(value.getClass())).toHash(value)).withStreamKey(value.getStream());
|
||||
}
|
||||
|
||||
default <V> ObjectRecord<K, V> toObjectRecord(MapRecord<K, HK, HV> entry, Class<V> targetType) {
|
||||
return entry.toObjectRecord(getHashMapper(targetType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class DefaultRedisElementWriter<T> implements RedisElementWriter<T> {
|
||||
@Override
|
||||
public ByteBuffer write(T value) {
|
||||
|
||||
if (serializer != null) {
|
||||
if (serializer != null && (value == null || serializer.canSerialize(value.getClass()))) {
|
||||
return ByteBuffer.wrap(serializer.serialize(value));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,17 @@ import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StreamOperations;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -45,7 +47,7 @@ import org.springframework.util.ErrorHandler;
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListenerContainer<K, V> {
|
||||
class DefaultStreamMessageListenerContainer<K, V extends Record<K, ?>> implements StreamMessageListenerContainer<K, V> {
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
@@ -53,6 +55,8 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
private final ErrorHandler errorHandler;
|
||||
private final StreamReadOptions readOptions;
|
||||
private final RedisTemplate<K, ?> template;
|
||||
private final StreamOperations<K, Object, Object> streamOperations;
|
||||
private final StreamMessageListenerContainerOptions<K, V> containerOptions;
|
||||
|
||||
private final List<Subscription> subscriptions = new ArrayList<>();
|
||||
|
||||
@@ -74,6 +78,13 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
this.errorHandler = containerOptions.getErrorHandler();
|
||||
this.readOptions = getStreamReadOptions(containerOptions);
|
||||
this.template = createRedisTemplate(connectionFactory, containerOptions);
|
||||
this.containerOptions = containerOptions;
|
||||
|
||||
if (containerOptions.getHashMapper() != null) {
|
||||
this.streamOperations = this.template.opsForStream(containerOptions.getHashMapper());
|
||||
} else {
|
||||
this.streamOperations = this.template.opsForStream();
|
||||
}
|
||||
}
|
||||
|
||||
private static StreamReadOptions getStreamReadOptions(StreamMessageListenerContainerOptions<?, ?> options) {
|
||||
@@ -92,9 +103,9 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
|
||||
RedisTemplate<K, V> template = new RedisTemplate<>();
|
||||
template.setKeySerializer(containerOptions.getKeySerializer());
|
||||
template.setValueSerializer(containerOptions.getBodySerializer());
|
||||
template.setHashKeySerializer(containerOptions.getKeySerializer());
|
||||
template.setHashValueSerializer(containerOptions.getBodySerializer());
|
||||
template.setValueSerializer(containerOptions.getKeySerializer());
|
||||
template.setHashKeySerializer(containerOptions.getHashKeySerializer());
|
||||
template.setHashValueSerializer(containerOptions.getHashValueSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
@@ -193,9 +204,16 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
return doRegister(getReadTask(streamRequest, listener));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StreamPollTask<K, V> getReadTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
|
||||
|
||||
StreamOperations<K, ?, ?> streamOperations = template.opsForStream();
|
||||
BiFunction<K, ReadOffset, List<? extends Record<?, ?>>> readFunction = getReadFunction(streamRequest);
|
||||
|
||||
return new StreamPollTask<>(streamRequest, listener, errorHandler, (BiFunction) readFunction);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private BiFunction<K, ReadOffset, List<? extends Record<?, ?>>> getReadFunction(StreamReadRequest<K> streamRequest) {
|
||||
|
||||
if (streamRequest instanceof StreamMessageListenerContainer.ConsumerStreamReadRequest) {
|
||||
|
||||
@@ -204,20 +222,20 @@ class DefaultStreamMessageListenerContainer<K, V> implements StreamMessageListen
|
||||
StreamReadOptions readOptions = consumerStreamRequest.isAutoAck() ? this.readOptions : this.readOptions.noack();
|
||||
Consumer consumer = consumerStreamRequest.getConsumer();
|
||||
|
||||
return new StreamPollTask<K, V>(consumerStreamRequest, listener, errorHandler, (key, offset) -> {
|
||||
|
||||
return (List<Record<K, V>>) (List) streamOperations.read(consumer, readOptions,
|
||||
if (this.containerOptions.getHashMapper() != null) {
|
||||
return (key, offset) -> streamOperations.read(this.containerOptions.getTargetType(), consumer, readOptions,
|
||||
StreamOffset.create(key, offset));
|
||||
// List<StreamMessage<K, V>> x = (List<StreamMessage<K, V>>)(List) streamOperations.read(consumer, readOptions,
|
||||
// StreamOffset.create(key, offset));
|
||||
// return x;
|
||||
}
|
||||
|
||||
});
|
||||
return (key, offset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, offset));
|
||||
}
|
||||
|
||||
return new StreamPollTask<>(streamRequest, listener, errorHandler, (key, offset) -> {
|
||||
return (List<Record<K, V>>) (List) streamOperations.read(readOptions, StreamOffset.create(key, offset));
|
||||
});
|
||||
if (this.containerOptions.getHashMapper() != null) {
|
||||
return (key, offset) -> streamOperations.read(this.containerOptions.getTargetType(), readOptions,
|
||||
StreamOffset.create(key, offset));
|
||||
}
|
||||
|
||||
return (key, offset) -> streamOperations.read(readOptions, StreamOffset.create(key, offset));
|
||||
}
|
||||
|
||||
private Subscription doRegister(Task task) {
|
||||
|
||||
@@ -34,14 +34,14 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamReadOptions;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.core.ReactiveStreamOperations;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link StreamReceiver}.
|
||||
@@ -49,11 +49,13 @@ import org.springframework.data.redis.serializer.RedisSerializationContext.Seria
|
||||
* @author Mark Paluch
|
||||
* @since 2.2
|
||||
*/
|
||||
class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
class DefaultStreamReceiver<K, V extends Record<K, ?>> implements StreamReceiver<K, V> {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final ReactiveRedisTemplate<K, ?> template;
|
||||
private final ReactiveStreamOperations<K, Object, Object> streamOperations;
|
||||
private final StreamReadOptions readOptions;
|
||||
private final StreamReceiverOptions<K, V> receiverOptions;
|
||||
|
||||
/**
|
||||
* Create a new {@link DefaultStreamReceiver} given {@link ReactiveRedisConnectionFactory} and
|
||||
@@ -62,13 +64,14 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
*/
|
||||
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, HK, HV> options) {
|
||||
@SuppressWarnings("unchecked")
|
||||
DefaultStreamReceiver(ReactiveRedisConnectionFactory connectionFactory, StreamReceiverOptions<K, V> options) {
|
||||
receiverOptions = options;
|
||||
|
||||
RedisSerializationContext<HK, HV> serializationContext = RedisSerializationContext
|
||||
.<HK, HV> newSerializationContext(options.getKeySerializer()) //
|
||||
.key((SerializationPair<HK>) options.getKeySerializer()) //
|
||||
.value((SerializationPair<HV>) options.getBodySerializer()) //
|
||||
.build();
|
||||
RedisSerializationContext<K, Object> serializationContext = RedisSerializationContext
|
||||
.<K, Object> newSerializationContext(options.getKeySerializer()) //
|
||||
.key(options.getKeySerializer()).hashKey(options.getHashKeySerializer())
|
||||
.hashValue(options.getHashValueSerializer()).build();
|
||||
|
||||
StreamReadOptions readOptions = StreamReadOptions.empty().count(options.getBatchSize());
|
||||
if (!options.getPollTimeout().isZero()) {
|
||||
@@ -77,6 +80,12 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
|
||||
this.readOptions = readOptions;
|
||||
this.template = new ReactiveRedisTemplate(connectionFactory, serializationContext);
|
||||
|
||||
if (options.getHashMapper() != null) {
|
||||
this.streamOperations = this.template.opsForStream(options.getHashMapper());
|
||||
} else {
|
||||
this.streamOperations = this.template.opsForStream();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -84,19 +93,27 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
|
||||
*/
|
||||
@Override
|
||||
public Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<V> receive(StreamOffset<K> streamOffset) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("receive(%s)", streamOffset));
|
||||
}
|
||||
|
||||
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction;
|
||||
|
||||
if (receiverOptions.getHashMapper() != null) {
|
||||
readFunction = (key, readOffset) -> streamOperations.read(receiverOptions.getTargetType(), readOptions,
|
||||
StreamOffset.create(key, readOffset));
|
||||
} else {
|
||||
readFunction = (key, readOffset) -> streamOperations.read(readOptions, StreamOffset.create(key, readOffset));
|
||||
}
|
||||
|
||||
return Flux.defer(() -> {
|
||||
|
||||
PollState pollState = PollState.standalone(streamOffset.getOffset());
|
||||
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
|
||||
.<HK, HV> opsForStream().read(readOptions, StreamOffset.create(key, readOffset));
|
||||
|
||||
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
|
||||
return Flux.create(
|
||||
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,19 +122,21 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @see org.springframework.data.redis.stream.StreamReceiver#receiveAutoAck(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
|
||||
*/
|
||||
@Override
|
||||
public Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<V> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("receiveAutoAck(%s, %s)", consumer, streamOffset));
|
||||
}
|
||||
|
||||
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction = getConsumeReadFunction(consumer,
|
||||
this.readOptions);
|
||||
|
||||
return Flux.defer(() -> {
|
||||
|
||||
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
|
||||
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
|
||||
.<HK, HV> opsForStream().read(consumer, readOptions, StreamOffset.create(key, readOffset));
|
||||
|
||||
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
|
||||
return Flux.create(
|
||||
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,36 +145,47 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
* @see org.springframework.data.redis.stream.StreamReceiver#receive(org.springframework.data.redis.connection.RedisStreamCommands.Consumer, org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset)
|
||||
*/
|
||||
@Override
|
||||
public Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<V> receive(Consumer consumer, StreamOffset<K> streamOffset) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("receive(%s, %s)", consumer, streamOffset));
|
||||
}
|
||||
|
||||
StreamReadOptions noack = readOptions.noack();
|
||||
BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> readFunction = getConsumeReadFunction(consumer,
|
||||
this.readOptions.noack());
|
||||
|
||||
return Flux.defer(() -> {
|
||||
|
||||
PollState pollState = PollState.consumer(consumer, streamOffset.getOffset());
|
||||
BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction = (key, readOffset) -> template
|
||||
.<HK, HV> opsForStream().read(consumer, noack, StreamOffset.create(key, readOffset));
|
||||
|
||||
return Flux.create(sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, readFunction).arm());
|
||||
return Flux.create(
|
||||
sink -> new StreamSubscription(sink, streamOffset.getKey(), pollState, (BiFunction) readFunction).arm());
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private BiFunction<K, ReadOffset, Flux<? extends Record<?, ?>>> getConsumeReadFunction(Consumer consumer,
|
||||
StreamReadOptions readOptions) {
|
||||
|
||||
if (receiverOptions.getHashMapper() != null) {
|
||||
return (key, readOffset) -> streamOperations.read(receiverOptions.getTargetType(), consumer, readOptions,
|
||||
StreamOffset.create(key, readOffset));
|
||||
}
|
||||
|
||||
return (key, readOffset) -> streamOperations.read(consumer, readOptions, StreamOffset.create(key, readOffset));
|
||||
}
|
||||
|
||||
/**
|
||||
* A stateful Redis Stream subscription.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class StreamSubscription {
|
||||
|
||||
private final Queue<MapRecord<K, HK, HV>> overflow = Queues.<MapRecord<K, HK, HV>> small().get();
|
||||
private final Queue<V> overflow = Queues.<V> small().get();
|
||||
|
||||
private final FluxSink<MapRecord<K, HK, HV>> sink;
|
||||
private final FluxSink<V> sink;
|
||||
private final K key;
|
||||
private final PollState pollState;
|
||||
private final BiFunction<K, ReadOffset, Flux<MapRecord<K, HK, HV>>> readFunction;
|
||||
private final BiFunction<K, ReadOffset, Flux<V>> readFunction;
|
||||
|
||||
/**
|
||||
* Arm the subscription so {@link Subscription#request(long) demand} activates polling.
|
||||
@@ -250,15 +280,15 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
String.format("[stream: %s] scheduleIfRequired(): Activating subscription, offset %s", key, readOffset));
|
||||
}
|
||||
|
||||
Flux<MapRecord<K, HK, HV>> poll = readFunction.apply(key, readOffset);
|
||||
Flux<V> poll = readFunction.apply(key, readOffset);
|
||||
|
||||
poll.subscribe(getSubscriber());
|
||||
}
|
||||
}
|
||||
|
||||
private CoreSubscriber<MapRecord<K, HK, HV>> getSubscriber() {
|
||||
private CoreSubscriber<V> getSubscriber() {
|
||||
|
||||
return new CoreSubscriber<MapRecord<K, HK, HV>>() {
|
||||
return new CoreSubscriber<V>() {
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
@@ -266,7 +296,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(MapRecord<K, HK, HV> message) {
|
||||
public void onNext(V message) {
|
||||
onStreamMessage(message);
|
||||
}
|
||||
|
||||
@@ -294,7 +324,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
};
|
||||
}
|
||||
|
||||
private void onStreamMessage(MapRecord<K, HK, HV> message) {
|
||||
private void onStreamMessage(V message) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("[stream: %s] onStreamMessage(%s)", key, message));
|
||||
@@ -359,7 +389,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
|
||||
if (demand == Long.MAX_VALUE) {
|
||||
|
||||
MapRecord<K, HK, HV> message = overflow.poll();
|
||||
V message = overflow.poll();
|
||||
|
||||
if (message == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -377,7 +407,7 @@ class DefaultStreamReceiver<K, HK, HV> implements StreamReceiver<K, HK, HV> {
|
||||
|
||||
} else if (pollState.setRequested(demand, demand - 1)) {
|
||||
|
||||
MapRecord<K, HK, HV> message = overflow.poll();
|
||||
V message = overflow.poll();
|
||||
|
||||
if (message == null) {
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
enum RawRedisSerializer implements RedisSerializer<byte[]> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public byte[] serialize(@Nullable byte[] bytes) throws SerializationException {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public byte[] deserialize(@Nullable byte[] bytes) throws SerializationException {
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ package org.springframework.data.redis.stream;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
|
||||
/**
|
||||
* Strategy to determine the first and subsequent {@link ReadOffset}.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
|
||||
/**
|
||||
* Listener interface to receive delivery of {@link Record messages}.
|
||||
@@ -26,12 +26,12 @@ import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
* @since 2.2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StreamListener<K, V> {
|
||||
public interface StreamListener<K, V extends Record<K, ?>> {
|
||||
|
||||
/**
|
||||
* Callback invoked on receiving a {@link Record}.
|
||||
*
|
||||
* @param message never {@literal null}.
|
||||
*/
|
||||
void onMessage(Record<K, V> message);
|
||||
void onMessage(V message);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,21 @@
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainer.LoggingErrorHandler;
|
||||
@@ -41,9 +42,9 @@ import org.springframework.util.ErrorHandler;
|
||||
* implemented externally.
|
||||
* <p/>
|
||||
* Once created, a {@link StreamMessageListenerContainer} can subscribe to a Redis Stream and consume incoming
|
||||
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and
|
||||
* returns a {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually
|
||||
* background polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
|
||||
* {@link Record messages}. {@link StreamMessageListenerContainer} allows multiple stream read requests and returns a
|
||||
* {@link Subscription} handle per read request. Cancelling the {@link Subscription} terminates eventually background
|
||||
* polling. Messages are converted using {@link RedisSerializer key and value serializers} to support various
|
||||
* serialization strategies. <br/>
|
||||
* {@link StreamMessageListenerContainer} supports multiple modes of stream consumption:
|
||||
* <ul>
|
||||
@@ -83,17 +84,17 @@ import org.springframework.util.ErrorHandler;
|
||||
* {@link StreamListener#onMessage(Record) listener callback}.
|
||||
* <p/>
|
||||
* {@link StreamMessageListenerContainer} tasks propagate errors during stream reads and
|
||||
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors
|
||||
* stop a {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows
|
||||
* conditional subscription cancelling or continuing on all errors.
|
||||
* {@link StreamListener#onMessage(Record) listener notification} to a configurable {@link ErrorHandler}. Errors stop a
|
||||
* {@link Subscription} by default. Configuring a {@link Predicate} for a {@link StreamReadRequest} allows conditional
|
||||
* subscription cancelling or continuing on all errors.
|
||||
* <p/>
|
||||
* See the following example code how to use {@link StreamMessageListenerContainer}:
|
||||
*
|
||||
* <pre class="code">
|
||||
* RedisConnectionFactory factory = …;
|
||||
*
|
||||
* StreamMessageListenerContainer<String, String> container = StreamMessageListenerContainer.create(factory);
|
||||
* Subscription subscription = container.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")), message -> …);
|
||||
* StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create(factory);
|
||||
* Subscription subscription = container.receive(StreamOffset.fromStart("my-stream"), message -> …);
|
||||
*
|
||||
* container.start();
|
||||
*
|
||||
@@ -115,7 +116,7 @@ import org.springframework.util.ErrorHandler;
|
||||
* @see RedisConnectionFactory
|
||||
* @see StreamReceiver
|
||||
*/
|
||||
public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
public interface StreamMessageListenerContainer<K, V extends Record<K, ?>> extends SmartLifecycle {
|
||||
|
||||
/**
|
||||
* Create a new {@link StreamMessageListenerContainer} using {@link StringRedisSerializer string serializers} given
|
||||
@@ -124,7 +125,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @return the new {@link StreamMessageListenerContainer}.
|
||||
*/
|
||||
static StreamMessageListenerContainer<String, Map<String, String>> create(RedisConnectionFactory connectionFactory) {
|
||||
static StreamMessageListenerContainer<String, MapRecord<String, String, String>> create(
|
||||
RedisConnectionFactory connectionFactory) {
|
||||
|
||||
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
|
||||
|
||||
@@ -140,7 +142,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the new {@link StreamMessageListenerContainer}.
|
||||
*/
|
||||
static <K, V> StreamMessageListenerContainer<K, V> create(RedisConnectionFactory connectionFactory,
|
||||
static <K, V extends Record<K, ?>> StreamMessageListenerContainer<K, V> create(
|
||||
RedisConnectionFactory connectionFactory,
|
||||
StreamMessageListenerContainerOptions<K, V> options) {
|
||||
|
||||
Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
|
||||
@@ -468,20 +471,38 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param <V> Stream value type.
|
||||
* @see StreamMessageListenerContainerOptionsBuilder
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class StreamMessageListenerContainerOptions<K, V> {
|
||||
class StreamMessageListenerContainerOptions<K, V extends Record<K, ?>> {
|
||||
|
||||
private final Duration pollTimeout;
|
||||
private final int batchSize;
|
||||
private final RedisSerializer<K> keySerializer;
|
||||
private final RedisSerializer<V> bodySerializer;
|
||||
private final RedisSerializer<Object> hashKeySerializer;
|
||||
private final RedisSerializer<Object> hashValueSerializer;
|
||||
private final @Nullable Class<Object> targetType;
|
||||
private final @Nullable HashMapper<Object, Object, Object> hashMapper;
|
||||
private final ErrorHandler errorHandler;
|
||||
private final Executor executor;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StreamMessageListenerContainerOptions(Duration pollTimeout, int batchSize, RedisSerializer<K> keySerializer,
|
||||
RedisSerializer<Object> hashKeySerializer, RedisSerializer<Object> hashValueSerializer,
|
||||
@Nullable Class<?> targetType, @Nullable HashMapper<V, ?, ?> hashMapper, ErrorHandler errorHandler,
|
||||
Executor executor) {
|
||||
this.pollTimeout = pollTimeout;
|
||||
this.batchSize = batchSize;
|
||||
this.keySerializer = keySerializer;
|
||||
this.hashKeySerializer = hashKeySerializer;
|
||||
this.hashValueSerializer = hashValueSerializer;
|
||||
this.targetType = (Class) targetType;
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
this.errorHandler = errorHandler;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new builder for {@link StreamMessageListenerContainerOptions}.
|
||||
*/
|
||||
static StreamMessageListenerContainerOptionsBuilder<String, Map<String, String>> builder() {
|
||||
static StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, String>> builder() {
|
||||
return new StreamMessageListenerContainerOptionsBuilder<>().serializer(StringRedisSerializer.UTF_8);
|
||||
}
|
||||
|
||||
@@ -507,8 +528,26 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
return keySerializer;
|
||||
}
|
||||
|
||||
public RedisSerializer<V> getBodySerializer() {
|
||||
return bodySerializer;
|
||||
public RedisSerializer<Object> getHashKeySerializer() {
|
||||
return hashKeySerializer;
|
||||
}
|
||||
|
||||
public RedisSerializer<Object> getHashValueSerializer() {
|
||||
return hashValueSerializer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public HashMapper<Object, Object, Object> getHashMapper() {
|
||||
return hashMapper;
|
||||
}
|
||||
|
||||
public Class<Object> getTargetType() {
|
||||
|
||||
if (this.targetType != null) {
|
||||
return targetType;
|
||||
}
|
||||
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -532,12 +571,16 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param <K> Stream key and Stream field type
|
||||
* @param <V> Stream value type
|
||||
*/
|
||||
class StreamMessageListenerContainerOptionsBuilder<K, V> {
|
||||
@SuppressWarnings("unchecked")
|
||||
class StreamMessageListenerContainerOptionsBuilder<K, V extends Record<K, ?>> {
|
||||
|
||||
private Duration pollTimeout = Duration.ofSeconds(2);
|
||||
private int batchSize = 1;
|
||||
private RedisSerializer<K> keySerializer;
|
||||
private RedisSerializer<V> bodySerializer;
|
||||
private RedisSerializer<Object> hashKeySerializer;
|
||||
private RedisSerializer<Object> hashValueSerializer;
|
||||
private @Nullable HashMapper<V, ?, ?> hashMapper;
|
||||
private @Nullable Class<?> targetType;
|
||||
private ErrorHandler errorHandler = LoggingErrorHandler.INSTANCE;
|
||||
private Executor executor = new SimpleAsyncTaskExecutor();
|
||||
|
||||
@@ -601,15 +644,19 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a key and value serializer.
|
||||
* Configure a key, hash key and hash value serializer.
|
||||
*
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <T> StreamMessageListenerContainerOptionsBuilder<T, Map<T, T>> serializer(RedisSerializer<T> serializer) {
|
||||
public <T> StreamMessageListenerContainerOptionsBuilder<T, MapRecord<T, T, T>> serializer(
|
||||
RedisSerializer<T> serializer) {
|
||||
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.keySerializer = (RedisSerializer) serializer;
|
||||
this.bodySerializer = (RedisSerializer) serializer;
|
||||
this.hashKeySerializer = (RedisSerializer) serializer;
|
||||
this.hashValueSerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -619,21 +666,81 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <NK> StreamMessageListenerContainerOptionsBuilder<NK, V> keySerializer(RedisSerializer<NK> serializer) {
|
||||
public <NK, NV extends Record<NK, ?>> StreamMessageListenerContainerOptionsBuilder<NK, NV> keySerializer(
|
||||
RedisSerializer<NK> serializer) {
|
||||
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.keySerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a value serializer.
|
||||
* Configure a hash key serializer.
|
||||
*
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <NV> StreamMessageListenerContainerOptionsBuilder<K, NV> bodySerializer(RedisSerializer<NV> serializer) {
|
||||
public <HK, HV> StreamMessageListenerContainerOptionsBuilder<K, MapRecord<K, HK, HV>> hashKeySerializer(
|
||||
RedisSerializer<HK> serializer) {
|
||||
|
||||
this.bodySerializer = (RedisSerializer) serializer;
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.hashKeySerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash value serializer.
|
||||
*
|
||||
* @param serializer must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
public <HK, HV> StreamMessageListenerContainerOptionsBuilder<K, MapRecord<K, HK, HV>> hashValueSerializer(
|
||||
RedisSerializer<HK> serializer) {
|
||||
|
||||
Assert.notNull(serializer, "RedisSerializer must not be null");
|
||||
|
||||
this.hashValueSerializer = (RedisSerializer) serializer;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash target type. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamMessageListenerContainerOptionsBuilder<K, ObjectRecord<K, NV>> targetType(Class<NV> targetType) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
this.targetType = targetType;
|
||||
|
||||
if (this.hashMapper == null) {
|
||||
|
||||
hashKeySerializer(RawRedisSerializer.INSTANCE);
|
||||
hashValueSerializer(RawRedisSerializer.INSTANCE);
|
||||
return (StreamMessageListenerContainerOptionsBuilder) objectMapper(new ObjectHashMapper());
|
||||
}
|
||||
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash mapper. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param hashMapper must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamMessageListenerContainerOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamMessageListenerContainerOptionsBuilder<K, ObjectRecord<K, NV>> objectMapper(
|
||||
HashMapper<NV, ?, ?> hashMapper) {
|
||||
|
||||
Assert.notNull(hashMapper, "HashMapper must not be null");
|
||||
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
return (StreamMessageListenerContainerOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -643,7 +750,8 @@ public interface StreamMessageListenerContainer<K, V> extends SmartLifecycle {
|
||||
* @return new {@link StreamMessageListenerContainerOptions}.
|
||||
*/
|
||||
public StreamMessageListenerContainerOptions<K, V> build() {
|
||||
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer,
|
||||
return new StreamMessageListenerContainerOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer,
|
||||
hashValueSerializer, targetType, hashMapper,
|
||||
errorHandler, executor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Record;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer.ConsumerStreamReadRequest;
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamReadRequest;
|
||||
import org.springframework.util.ErrorHandler;
|
||||
@@ -38,19 +38,19 @@ import org.springframework.util.ErrorHandler;
|
||||
* @author Mark Paluch
|
||||
* @see 2.2
|
||||
*/
|
||||
class StreamPollTask<K, V> implements Task {
|
||||
class StreamPollTask<K, V extends Record<K, ?>> implements Task {
|
||||
|
||||
private final StreamReadRequest<K> request;
|
||||
private final StreamListener<K, V> listener;
|
||||
private final ErrorHandler errorHandler;
|
||||
private final Predicate<Throwable> cancelSubscriptionOnError;
|
||||
private final BiFunction<K, ReadOffset, List<Record<K, V>>> readFunction;
|
||||
private final BiFunction<K, ReadOffset, List<V>> readFunction;
|
||||
|
||||
private final PollState pollState;
|
||||
private volatile boolean isInEventLoop = false;
|
||||
|
||||
StreamPollTask(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener, ErrorHandler errorHandler,
|
||||
BiFunction<K, ReadOffset, List<Record<K, V>>> readFunction) {
|
||||
BiFunction<K, ReadOffset, List<V>> readFunction) {
|
||||
|
||||
this.request = streamRequest;
|
||||
this.listener = listener;
|
||||
@@ -135,9 +135,9 @@ class StreamPollTask<K, V> implements Task {
|
||||
// allow interruption
|
||||
Thread.sleep(0);
|
||||
|
||||
List<Record<K, V>> read = readFunction.apply(key, pollState.getCurrentReadOffset());
|
||||
List<V> read = readFunction.apply(key, pollState.getCurrentReadOffset());
|
||||
|
||||
for (Record<K, V> message : read) {
|
||||
for (V message : read) {
|
||||
|
||||
listener.onMessage(message);
|
||||
pollState.updateReadOffset(message.getId().getValue());
|
||||
|
||||
@@ -15,27 +15,33 @@
|
||||
*/
|
||||
package org.springframework.data.redis.stream;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.MapRecord;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.Consumer;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.ReadOffset;
|
||||
import org.springframework.data.redis.connection.RedisStreamCommands.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.Record;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.hash.HashMapper;
|
||||
import org.springframework.data.redis.hash.ObjectHashMapper;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A receiver to consume Redis Streams using reactive infrastructure.
|
||||
* <p/>
|
||||
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link org.springframework.data.redis.connection.RedisStreamCommands.Record
|
||||
* messages}. Consider a {@link Flux} of {@link Record} infinite. Cancelling the
|
||||
* {@link org.reactivestreams.Subscription} terminates eventually background polling. Messages are converted using
|
||||
* {@link SerializationPair key and value serializers} to support various serialization strategies. <br/>
|
||||
* Once created, a {@link StreamReceiver} can subscribe to a Redis Stream and consume incoming {@link Record records}.
|
||||
* Consider a {@link Flux} of {@link Record} infinite. Cancelling the {@link org.reactivestreams.Subscription}
|
||||
* terminates eventually background polling. Records are converted using {@link SerializationPair key and value
|
||||
* serializers} to support various serialization strategies. <br/>
|
||||
* {@link StreamReceiver} supports three modes of stream consumption:
|
||||
* <ul>
|
||||
* <li>Standalone</li>
|
||||
@@ -49,36 +55,36 @@ import org.springframework.util.Assert;
|
||||
* <br/>
|
||||
* <strong>Standalone</strong>
|
||||
* <ul>
|
||||
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
|
||||
* last seen {@link Record#getId() message Id}.</li>
|
||||
* <li>{@link ReadOffset#from(String)} Offset using a particular record Id: Start with the given offset and use the last
|
||||
* seen {@link Record#getId() record Id}.</li>
|
||||
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the latest offset ({@code $}) and use the last seen
|
||||
* {@link Record#getId() message Id}.</li>
|
||||
* {@link Record#getId() record Id}.</li>
|
||||
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
|
||||
* ({@code $}) for subsequent reads.</li>
|
||||
* </ul>
|
||||
* <br/>
|
||||
* <strong>Using {@link Consumer}</strong>
|
||||
* <ul>
|
||||
* <li>{@link ReadOffset#from(String)} Offset using a particular message Id: Start with the given offset and use the
|
||||
* last seen {@link Record#getId() message Id}.</li>
|
||||
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed message by the consumer ({@code >})
|
||||
* and use the last consumed message by the consumer ({@code >}) for subsequent reads.</li>
|
||||
* <li>{@link ReadOffset#from(String)} Offset using a particular record Id: Start with the given offset and use the last
|
||||
* seen {@link Record#getId() record Id}.</li>
|
||||
* <li>{@link ReadOffset#lastConsumed()} Last consumed: Start with the last consumed record by the consumer ({@code >})
|
||||
* and use the last consumed record by the consumer ({@code >}) for subsequent reads.</li>
|
||||
* <li>{@link ReadOffset#latest()} Last consumed: Start with the latest offset ({@code $}) and use latest offset
|
||||
* ({@code $}) for subsequent reads.</li>
|
||||
* </ul>
|
||||
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped messages as messages can arrive in the
|
||||
* time during polling is suspended. Use messagedId's as offset or {@link ReadOffset#lastConsumed()} to minimize the
|
||||
* chance of message loss.</strong>
|
||||
* <strong>Note: Using {@link ReadOffset#latest()} bears the chance of dropped records as records can arrive in the time
|
||||
* during polling is suspended. Use recorddId's as offset or {@link ReadOffset#lastConsumed()} to minimize the chance of
|
||||
* record loss.</strong>
|
||||
* <p/>
|
||||
* See the following example code how to use {@link StreamReceiver}:
|
||||
*
|
||||
* <pre class="code">
|
||||
* ReactiveRedisConnectionFactory factory = …;
|
||||
*
|
||||
* StreamReceiver<String, String> receiver = StreamReceiver.create(factory);
|
||||
* Flux<StreamMessage<String, String>> messages = receiver.receive(StreamOffset.create("my-stream", ReadOffset.from("0-0")));
|
||||
* StreamReceiver<String, String, String> receiver = StreamReceiver.create(factory);
|
||||
* Flux<MapRecord<String, String, String>> records = receiver.receive(StreamOffset.fromStart("my-stream"));
|
||||
*
|
||||
* messageFlux.doOnNext(message -> …);
|
||||
* recordFlux.doOnNext(record -> …);
|
||||
* </pre>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
@@ -90,7 +96,7 @@ import org.springframework.util.Assert;
|
||||
* @see ReactiveRedisConnectionFactory
|
||||
* @see StreamMessageListenerContainer
|
||||
*/
|
||||
public interface StreamReceiver<K, HK, HV> {
|
||||
public interface StreamReceiver<K, V extends Record<K, ?>> {
|
||||
|
||||
/**
|
||||
* Create a new {@link StreamReceiver} using {@link StringRedisSerializer string serializers} given
|
||||
@@ -99,7 +105,8 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param connectionFactory must not be {@literal null}.
|
||||
* @return the new {@link StreamReceiver}.
|
||||
*/
|
||||
static StreamReceiver<String, String, String> create(ReactiveRedisConnectionFactory connectionFactory) {
|
||||
static StreamReceiver<String, MapRecord<String, String, String>> create(
|
||||
ReactiveRedisConnectionFactory connectionFactory) {
|
||||
|
||||
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
|
||||
|
||||
@@ -114,8 +121,8 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the new {@link StreamReceiver}.
|
||||
*/
|
||||
static <K, HK, HV> StreamReceiver<K, HK, HV> create(ReactiveRedisConnectionFactory connectionFactory,
|
||||
StreamReceiverOptions<K, HK, HV> options) {
|
||||
static <K, V extends Record<K, ?>> StreamReceiver<K, V> create(ReactiveRedisConnectionFactory connectionFactory,
|
||||
StreamReceiverOptions<K, V> options) {
|
||||
|
||||
Assert.notNull(connectionFactory, "ReactiveRedisConnectionFactory must not be null!");
|
||||
Assert.notNull(options, "StreamReceiverOptions must not be null!");
|
||||
@@ -124,25 +131,25 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
|
||||
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
|
||||
* receiver is closed when the returned {@link Flux} terminates.
|
||||
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
|
||||
* are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The receiver
|
||||
* is closed when the returned {@link Flux} terminates.
|
||||
* <p/>
|
||||
* Every message must be acknowledged using
|
||||
* Every record must be acknowledged using
|
||||
* {@link org.springframework.data.redis.connection.ReactiveStreamCommands#xAck(ByteBuffer, String, String...)}
|
||||
*
|
||||
* @param streamOffset the stream along its offset.
|
||||
* @return Flux of inbound {@link Record}s.
|
||||
* @see StreamOffset#create(Object, ReadOffset)
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> receive(StreamOffset<K> streamOffset);
|
||||
Flux<V> receive(StreamOffset<K> streamOffset);
|
||||
|
||||
/**
|
||||
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
|
||||
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
|
||||
* receiver is closed when the returned {@link Flux} terminates.
|
||||
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
|
||||
* are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The receiver
|
||||
* is closed when the returned {@link Flux} terminates.
|
||||
* <p/>
|
||||
* Every message is acknowledged when received.
|
||||
* Every record is acknowledged when received.
|
||||
*
|
||||
* @param consumer consumer group, must not be {@literal null}.
|
||||
* @param streamOffset the stream along its offset.
|
||||
@@ -150,14 +157,14 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @see StreamOffset#create(Object, ReadOffset)
|
||||
* @see ReadOffset#lastConsumed()
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
Flux<V> receiveAutoAck(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
|
||||
/**
|
||||
* Starts a Redis Stream consumer that consumes {@link Record messages} from the {@link StreamOffset stream}.
|
||||
* Messages are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The
|
||||
* receiver is closed when the returned {@link Flux} terminates.
|
||||
* Starts a Redis Stream consumer that consumes {@link Record records} from the {@link StreamOffset stream}. Records
|
||||
* are consumed from Redis and delivered on the returned {@link Flux} when requests are made on the Flux. The receiver
|
||||
* is closed when the returned {@link Flux} terminates.
|
||||
* <p/>
|
||||
* Every message must be acknowledged using
|
||||
* Every record must be acknowledged using
|
||||
* {@link org.springframework.data.redis.core.ReactiveStreamOperations#acknowledge(Object, String, String...)} after
|
||||
* processing.
|
||||
*
|
||||
@@ -167,7 +174,7 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @see StreamOffset#create(Object, ReadOffset)
|
||||
* @see ReadOffset#lastConsumed()
|
||||
*/
|
||||
Flux<MapRecord<K, HK, HV>> receive(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
Flux<V> receive(Consumer consumer, StreamOffset<K> streamOffset);
|
||||
|
||||
/**
|
||||
* Options for {@link StreamReceiver}.
|
||||
@@ -176,32 +183,51 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param <V> Stream value type.
|
||||
* @see StreamReceiverOptionsBuilder
|
||||
*/
|
||||
class StreamReceiverOptions<K, HK, HV> {
|
||||
class StreamReceiverOptions<K, V extends Record<K, ?>> {
|
||||
|
||||
private final Duration pollTimeout;
|
||||
private final int batchSize;
|
||||
private final SerializationPair<K> keySerializer;
|
||||
private final SerializationPair<HK> bodySerializer;
|
||||
private final SerializationPair<HV> vaueSerializer;
|
||||
private final SerializationPair<Object> hashKeySerializer;
|
||||
private final SerializationPair<Object> hashValueSerializer;
|
||||
private final @Nullable Class<Object> targetType;
|
||||
private final @Nullable HashMapper<Object, Object, Object> hashMapper;
|
||||
|
||||
private StreamReceiverOptions(Duration pollTimeout, int batchSize, SerializationPair<K> keySerializer,
|
||||
SerializationPair<HK> bodySerializer, SerializationPair<HV> valueSerializer) {
|
||||
SerializationPair<Object> hashKeySerializer, SerializationPair<Object> hashValueSerializer,
|
||||
@Nullable Class<?> targetType, @Nullable HashMapper<V, ?, ?> hashMapper) {
|
||||
|
||||
this.pollTimeout = pollTimeout;
|
||||
this.batchSize = batchSize;
|
||||
this.keySerializer = keySerializer;
|
||||
this.bodySerializer = bodySerializer;
|
||||
this.vaueSerializer = valueSerializer;
|
||||
this.hashKeySerializer = hashKeySerializer;
|
||||
this.hashValueSerializer = hashValueSerializer;
|
||||
this.targetType = (Class) targetType;
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new builder for {@link StreamReceiverOptions}.
|
||||
*/
|
||||
static StreamReceiverOptionsBuilder<String, String, String> builder() {
|
||||
public static StreamReceiverOptionsBuilder<String, MapRecord<String, String, String>> builder() {
|
||||
|
||||
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
|
||||
return new StreamReceiverOptionsBuilder<>().serializer(serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new builder for {@link StreamReceiverOptions}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> StreamReceiverOptionsBuilder<String, ObjectRecord<String, T>> builder(
|
||||
HashMapper<T, byte[], byte[]> hashMapper) {
|
||||
|
||||
SerializationPair<String> serializer = SerializationPair.fromSerializer(StringRedisSerializer.UTF_8);
|
||||
SerializationPair<ByteBuffer> raw = SerializationPair.raw();
|
||||
return new StreamReceiverOptionsBuilder<>().keySerializer(serializer).hashKeySerializer(raw)
|
||||
.hashValueSerializer(raw).objectMapper(hashMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout for blocking polling using the {@code BLOCK} option during reads.
|
||||
*
|
||||
@@ -224,8 +250,26 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
return keySerializer;
|
||||
}
|
||||
|
||||
public SerializationPair<HK> getBodySerializer() {
|
||||
return bodySerializer;
|
||||
public SerializationPair<Object> getHashKeySerializer() {
|
||||
return hashKeySerializer;
|
||||
}
|
||||
|
||||
public SerializationPair<Object> getHashValueSerializer() {
|
||||
return hashValueSerializer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public HashMapper<Object, Object, Object> getHashMapper() {
|
||||
return hashMapper;
|
||||
}
|
||||
|
||||
public Class<Object> getTargetType() {
|
||||
|
||||
if (this.targetType != null) {
|
||||
return targetType;
|
||||
}
|
||||
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,13 +278,15 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
*
|
||||
* @param <K> Stream key and Stream field type.
|
||||
*/
|
||||
class StreamReceiverOptionsBuilder<K, HK, HV> {
|
||||
class StreamReceiverOptionsBuilder<K, V extends Record<K, ?>> {
|
||||
|
||||
private Duration pollTimeout = Duration.ofSeconds(2);
|
||||
private int batchSize = 1;
|
||||
private SerializationPair<K> keySerializer;
|
||||
private SerializationPair<HK> bodySerializer;
|
||||
private SerializationPair<HV> valueSerializer;
|
||||
private SerializationPair<Object> hashKeySerializer;
|
||||
private SerializationPair<Object> hashValueSerializer;
|
||||
private @Nullable HashMapper<V, ?, ?> hashMapper;
|
||||
private @Nullable Class<?> targetType;
|
||||
|
||||
private StreamReceiverOptionsBuilder() {}
|
||||
|
||||
@@ -250,7 +296,7 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param pollTimeout must not be {@literal null} or negative.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public StreamReceiverOptionsBuilder<K, HK, HV> pollTimeout(Duration pollTimeout) {
|
||||
public StreamReceiverOptionsBuilder<K, V> pollTimeout(Duration pollTimeout) {
|
||||
|
||||
Assert.notNull(pollTimeout, "Poll timeout must not be null!");
|
||||
Assert.isTrue(!pollTimeout.isNegative(), "Poll timeout must not be negative!");
|
||||
@@ -262,28 +308,48 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
/**
|
||||
* Configure a batch size for the {@code COUNT} option during reading.
|
||||
*
|
||||
* @param messagesPerPoll must not be greater zero.
|
||||
* @param recordsPerPoll must not be greater zero.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public StreamReceiverOptionsBuilder<K, HK, HV> batchSize(int messagesPerPoll) {
|
||||
public StreamReceiverOptionsBuilder<K, V> batchSize(int recordsPerPoll) {
|
||||
|
||||
Assert.isTrue(messagesPerPoll > 0, "Batch size must be greater zero!");
|
||||
Assert.isTrue(recordsPerPoll > 0, "Batch size must be greater zero!");
|
||||
|
||||
this.batchSize = messagesPerPoll;
|
||||
this.batchSize = recordsPerPoll;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a key and value serializer.
|
||||
* Configure a key, hash key and hash value serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <T> StreamReceiverOptionsBuilder<T, T, T> serializer(SerializationPair<T> pair) {
|
||||
public <T> StreamReceiverOptionsBuilder<T, MapRecord<T, T, T>> serializer(SerializationPair<T> pair) {
|
||||
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.keySerializer = (SerializationPair) pair;
|
||||
this.bodySerializer = (SerializationPair) pair;
|
||||
this.valueSerializer = (SerializationPair) pair;
|
||||
this.hashKeySerializer = (SerializationPair) pair;
|
||||
this.hashValueSerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a key, hash key and hash value serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <T> StreamReceiverOptionsBuilder<T, MapRecord<T, T, T>> serializer(
|
||||
RedisSerializationContext<T, ?> serializationContext) {
|
||||
|
||||
Assert.notNull(serializationContext, "RedisSerializationContext must not be null");
|
||||
|
||||
this.keySerializer = (SerializationPair) serializationContext.getKeySerializationPair();
|
||||
this.hashKeySerializer = serializationContext.getHashKeySerializationPair();
|
||||
this.hashValueSerializer = serializationContext.getHashValueSerializationPair();
|
||||
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -293,21 +359,80 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <NK> StreamReceiverOptionsBuilder<NK, HK, HV> keySerializer(SerializationPair<NK> pair) {
|
||||
public <NK, NV extends Record<NK, ?>> StreamReceiverOptionsBuilder<NK, NV> keySerializer(
|
||||
SerializationPair<NK> pair) {
|
||||
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.keySerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a value serializer.
|
||||
* Configure a hash key serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <NV> StreamReceiverOptionsBuilder<K, HK, HV> bodySerializer(SerializationPair<NV> pair) {
|
||||
public <HK, HV> StreamReceiverOptionsBuilder<K, MapRecord<K, HK, HV>> hashKeySerializer(
|
||||
SerializationPair<HK> pair) {
|
||||
|
||||
this.bodySerializer = (SerializationPair) pair;
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.hashKeySerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash value serializer.
|
||||
*
|
||||
* @param pair must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
public <HK, HV> StreamReceiverOptionsBuilder<K, MapRecord<K, HK, HV>> hashValueSerializer(
|
||||
SerializationPair<HK> pair) {
|
||||
|
||||
Assert.notNull(pair, "SerializationPair must not be null");
|
||||
|
||||
this.hashValueSerializer = (SerializationPair) pair;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash target type. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param targetType must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamReceiverOptionsBuilder<K, ObjectRecord<K, NV>> targetType(Class<NV> targetType) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
this.targetType = targetType;
|
||||
|
||||
if (this.hashMapper == null) {
|
||||
|
||||
hashKeySerializer(SerializationPair.raw());
|
||||
hashValueSerializer(SerializationPair.raw());
|
||||
return (StreamReceiverOptionsBuilder) objectMapper(new ObjectHashMapper());
|
||||
}
|
||||
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a hash mapper. Changes the emitted {@link Record} type to {@link ObjectRecord}.
|
||||
*
|
||||
* @param hashMapper must not be {@literal null}.
|
||||
* @return {@code this} {@link StreamReceiverOptionsBuilder}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <NV> StreamReceiverOptionsBuilder<K, ObjectRecord<K, NV>> objectMapper(HashMapper<NV, ?, ?> hashMapper) {
|
||||
|
||||
Assert.notNull(hashMapper, "HashMapper must not be null");
|
||||
|
||||
this.hashMapper = (HashMapper) hashMapper;
|
||||
return (StreamReceiverOptionsBuilder) this;
|
||||
}
|
||||
|
||||
@@ -316,8 +441,9 @@ public interface StreamReceiver<K, HK, HV> {
|
||||
*
|
||||
* @return new {@link StreamReceiverOptions}.
|
||||
*/
|
||||
public StreamReceiverOptions<K, HK, HV> build() {
|
||||
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, bodySerializer, valueSerializer);
|
||||
public StreamReceiverOptions<K, V> build() {
|
||||
return new StreamReceiverOptions<>(pollTimeout, batchSize, keySerializer, hashKeySerializer, hashValueSerializer,
|
||||
targetType, hashMapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user