/*
* Copyright 2013-2017 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.jedis;
import redis.clients.jedis.BinaryClient.LIST_POSITION;
import redis.clients.jedis.BitOP;
import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.SortingParams;
import redis.clients.jedis.params.geo.GeoRadiusParam;
import redis.clients.util.SafeEncoder;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.DefaultTuple;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.Flag;
import org.springframework.data.redis.connection.RedisListCommands.Position;
import org.springframework.data.redis.connection.RedisServer;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.connection.RedisZSetCommands.Range.Boundary;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.connection.SortParameters;
import org.springframework.data.redis.connection.SortParameters.Order;
import org.springframework.data.redis.connection.SortParameters.Range;
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.convert.StringToRedisClientInfoConverter;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Jedis type converters.
*
* @author Jennifer Hickey
* @author Christoph Strobl
* @author Thomas Darimont
* @author Jungtaek Lim
* @author Mark Paluch
* @author Ninad Divadkar
*/
abstract public class JedisConverters extends Converters {
private static final Converter<String, byte[]> STRING_TO_BYTES;
private static final ListConverter<String, byte[]> STRING_LIST_TO_BYTE_LIST;
private static final SetConverter<String, byte[]> STRING_SET_TO_BYTE_SET;
private static final MapConverter<String, byte[]> STRING_MAP_TO_BYTE_MAP;
private static final SetConverter<redis.clients.jedis.Tuple, Tuple> TUPLE_SET_TO_TUPLE_SET;
private static final Converter<Exception, DataAccessException> EXCEPTION_CONVERTER = new JedisExceptionConverter();
private static final Converter<String[], List<RedisClientInfo>> STRING_TO_CLIENT_INFO_CONVERTER = new StringToRedisClientInfoConverter();
private static final Converter<redis.clients.jedis.Tuple, Tuple> TUPLE_CONVERTER;
private static final ListConverter<redis.clients.jedis.Tuple, Tuple> TUPLE_LIST_TO_TUPLE_LIST_CONVERTER;
private static final Converter<Object, RedisClusterNode> OBJECT_TO_CLUSTER_NODE_CONVERTER;
private static final Converter<Expiration, byte[]> EXPIRATION_TO_COMMAND_OPTION_CONVERTER;
private static final Converter<SetOption, byte[]> SET_OPTION_TO_COMMAND_OPTION_CONVERTER;
private static final Converter<List<String>, Long> STRING_LIST_TO_TIME_CONVERTER;
private static final Converter<redis.clients.jedis.GeoCoordinate, Point> GEO_COORDINATE_TO_POINT_CONVERTER;
private static final ListConverter<redis.clients.jedis.GeoCoordinate, Point> LIST_GEO_COORDINATE_TO_POINT_CONVERTER;
private static final Converter<byte[], String> BYTES_TO_STRING_CONVERTER;
private static final ListConverter<byte[], String> BYTES_LIST_TO_STRING_LIST_CONVERTER;
public static final byte[] PLUS_BYTES;
public static final byte[] MINUS_BYTES;
public static final byte[] POSITIVE_INFINITY_BYTES;
public static final byte[] NEGATIVE_INFINITY_BYTES;
private static final byte[] EX;
private static final byte[] PX;
private static final byte[] NX;
private static final byte[] XX;
static {
BYTES_TO_STRING_CONVERTER = new Converter<byte[], String>() {
@Override
public String convert(byte[] source) {
return source == null ? null : SafeEncoder.encode(source);
}
};
BYTES_LIST_TO_STRING_LIST_CONVERTER = new ListConverter<>(BYTES_TO_STRING_CONVERTER);
STRING_TO_BYTES = new Converter<String, byte[]>() {
public byte[] convert(String source) {
return source == null ? null : SafeEncoder.encode(source);
}
};
STRING_LIST_TO_BYTE_LIST = new ListConverter<>(STRING_TO_BYTES);
STRING_SET_TO_BYTE_SET = new SetConverter<>(STRING_TO_BYTES);
STRING_MAP_TO_BYTE_MAP = new MapConverter<>(STRING_TO_BYTES);
TUPLE_CONVERTER = new Converter<redis.clients.jedis.Tuple, Tuple>() {
public Tuple convert(redis.clients.jedis.Tuple source) {
return source != null ? new DefaultTuple(source.getBinaryElement(), source.getScore()) : null;
}
};
TUPLE_SET_TO_TUPLE_SET = new SetConverter<>(TUPLE_CONVERTER);
TUPLE_LIST_TO_TUPLE_LIST_CONVERTER = new ListConverter<>(TUPLE_CONVERTER);
PLUS_BYTES = toBytes("+");
MINUS_BYTES = toBytes("-");
POSITIVE_INFINITY_BYTES = toBytes("+inf");
NEGATIVE_INFINITY_BYTES = toBytes("-inf");
OBJECT_TO_CLUSTER_NODE_CONVERTER = new Converter<Object, RedisClusterNode>() {
@Override
public RedisClusterNode convert(Object infos) {
List<Object> values = (List<Object>) infos;
RedisClusterNode.SlotRange range = new RedisClusterNode.SlotRange(((Number) values.get(0)).intValue(),
((Number) values.get(1)).intValue());
List<Object> nodeInfo = (List<Object>) values.get(2);
return new RedisClusterNode(JedisConverters.toString((byte[]) nodeInfo.get(0)),
((Number) nodeInfo.get(1)).intValue(), range);
}
};
EX = toBytes("EX");
PX = toBytes("PX");
EXPIRATION_TO_COMMAND_OPTION_CONVERTER = new Converter<Expiration, byte[]>() {
@Override
public byte[] convert(Expiration source) {
if (source == null || source.isPersistent()) {
return new byte[0];
}
if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, source.getTimeUnit())) {
return PX;
}
return EX;
}
};
NX = toBytes("NX");
XX = toBytes("XX");
SET_OPTION_TO_COMMAND_OPTION_CONVERTER = new Converter<RedisStringCommands.SetOption, byte[]>() {
@Override
public byte[] convert(SetOption source) {
switch (source) {
case UPSERT:
return new byte[0];
case SET_IF_ABSENT:
return NX;
case SET_IF_PRESENT:
return XX;
}
throw new IllegalArgumentException(String.format("Invalid argument %s for SetOption.", source));
}
};
STRING_LIST_TO_TIME_CONVERTER = new Converter<List<String>, Long>() {
@Override
public Long convert(List<String> source) {
Assert.notEmpty(source, "Received invalid result from server. Expected 2 items in collection.");
Assert.isTrue(source.size() == 2,
"Received invalid nr of arguments from redis server. Expected 2 received " + source.size());
return toTimeMillis(source.get(0), source.get(1));
}
};
GEO_COORDINATE_TO_POINT_CONVERTER = new Converter<redis.clients.jedis.GeoCoordinate, Point>() {
@Override
public Point convert(redis.clients.jedis.GeoCoordinate geoCoordinate) {
return geoCoordinate != null ? new Point(geoCoordinate.getLongitude(), geoCoordinate.getLatitude()) : null;
}
};
LIST_GEO_COORDINATE_TO_POINT_CONVERTER = new ListConverter<>(GEO_COORDINATE_TO_POINT_CONVERTER);
}
public static Converter<String, byte[]> stringToBytes() {
return STRING_TO_BYTES;
}
/**
* {@link ListConverter} converting jedis {@link redis.clients.jedis.Tuple} to {@link Tuple}.
*
* @return
* @since 1.4
*/
public static ListConverter<redis.clients.jedis.Tuple, Tuple> tuplesToTuples() {
return TUPLE_LIST_TO_TUPLE_LIST_CONVERTER;
}
public static ListConverter<String, byte[]> stringListToByteList() {
return STRING_LIST_TO_BYTE_LIST;
}
public static SetConverter<String, byte[]> stringSetToByteSet() {
return STRING_SET_TO_BYTE_SET;
}
public static MapConverter<String, byte[]> stringMapToByteMap() {
return STRING_MAP_TO_BYTE_MAP;
}
public static SetConverter<redis.clients.jedis.Tuple, Tuple> tupleSetToTupleSet() {
return TUPLE_SET_TO_TUPLE_SET;
}
public static Converter<Exception, DataAccessException> exceptionConverter() {
return EXCEPTION_CONVERTER;
}
public static String[] toStrings(byte[][] source) {
String[] result = new String[source.length];
for (int i = 0; i < source.length; i++) {
result[i] = SafeEncoder.encode(source[i]);
}
return result;
}
public static Set<Tuple> toTupleSet(Set<redis.clients.jedis.Tuple> source) {
return TUPLE_SET_TO_TUPLE_SET.convert(source);
}
public static byte[] toBytes(Integer source) {
return String.valueOf(source).getBytes();
}
public static byte[] toBytes(Long source) {
return String.valueOf(source).getBytes();
}
/**
* @param source
* @return
* @since 1.6
*/
public static byte[] toBytes(Double source) {
return toBytes(String.valueOf(source));
}
public static byte[] toBytes(String source) {
return STRING_TO_BYTES.convert(source);
}
public static String toString(byte[] source) {
return source == null ? null : SafeEncoder.encode(source);
}
/**
* @param source
* @return
* @since 1.7
*/
public static RedisClusterNode toNode(Object source) {
return OBJECT_TO_CLUSTER_NODE_CONVERTER.convert(source);
}
/**
* @param source
* @return
* @since 1.3
*/
public static List<RedisClientInfo> toListOfRedisClientInformation(String source) {
if (!StringUtils.hasText(source)) {
return Collections.emptyList();
}
return STRING_TO_CLIENT_INFO_CONVERTER.convert(source.split("\\r?\\n"));
}
/**
* @param source
* @return
* @since 1.4
*/
public static List<RedisServer> toListOfRedisServer(List<Map<String, String>> source) {
if (CollectionUtils.isEmpty(source)) {
return Collections.emptyList();
}
List<RedisServer> sentinels = new ArrayList<>();
for (Map<String, String> info : source) {
sentinels.add(RedisServer.newServerFrom(Converters.toProperties(info)));
}
return sentinels;
}
public static DataAccessException toDataAccessException(Exception ex) {
return EXCEPTION_CONVERTER.convert(ex);
}
public static LIST_POSITION toListPosition(Position source) {
Assert.notNull(source, "list positions are mandatory");
return (Position.AFTER.equals(source) ? LIST_POSITION.AFTER : LIST_POSITION.BEFORE);
}
public static byte[][] toByteArrays(Map<byte[], byte[]> source) {
byte[][] result = new byte[source.size() * 2][];
int index = 0;
for (Map.Entry<byte[], byte[]> entry : source.entrySet()) {
result[index++] = entry.getKey();
result[index++] = entry.getValue();
}
return result;
}
public static SortingParams toSortingParams(SortParameters params) {
SortingParams jedisParams = null;
if (params != null) {
jedisParams = new SortingParams();
byte[] byPattern = params.getByPattern();
if (byPattern != null) {
jedisParams.by(params.getByPattern());
}
byte[][] getPattern = params.getGetPattern();
if (getPattern != null) {
jedisParams.get(getPattern);
}
Range limit = params.getLimit();
if (limit != null) {
jedisParams.limit((int) limit.getStart(), (int) limit.getCount());
}
Order order = params.getOrder();
if (order != null && order.equals(Order.DESC)) {
jedisParams.desc();
}
Boolean isAlpha = params.isAlphabetic();
if (isAlpha != null && isAlpha) {
jedisParams.alpha();
}
}
return jedisParams;
}
public static BitOP toBitOp(BitOperation bitOp) {
switch (bitOp) {
case AND:
return BitOP.AND;
case OR:
return BitOP.OR;
case NOT:
return BitOP.NOT;
case XOR:
return BitOP.XOR;
default:
throw new IllegalArgumentException();
}
}
/**
* Converts a given {@link Boundary} to its binary representation suitable for {@literal ZRANGEBY*} commands, despite
* {@literal ZRANGEBYLEX}.
*
* @param boundary
* @param defaultValue
* @return
* @since 1.6
*/
public static byte[] boundaryToBytesForZRange(Boundary boundary, byte[] defaultValue) {
if (boundary == null || boundary.getValue() == null) {
return defaultValue;
}
return boundaryToBytes(boundary, new byte[] {}, toBytes("("));
}
/**
* Converts a given {@link Boundary} to its binary representation suitable for ZRANGEBYLEX command.
*
* @param boundary
* @return
* @since 1.6
*/
public static byte[] boundaryToBytesForZRangeByLex(Boundary boundary, byte[] defaultValue) {
if (boundary == null || boundary.getValue() == null) {
return defaultValue;
}
return boundaryToBytes(boundary, toBytes("["), toBytes("("));
}
/**
* Converts a given {@link Expiration} to the according {@code SET} command argument.<br />
* <dl>
* <dt>{@link TimeUnit#SECONDS}</dt>
* <dd>{@code EX}</dd>
* <dt>{@link TimeUnit#MILLISECONDS}</dt>
* <dd>{@code PX}</dd>
* </dl>
*
* @param expiration
* @return
* @since 1.7
*/
public static byte[] toSetCommandExPxArgument(Expiration expiration) {
return EXPIRATION_TO_COMMAND_OPTION_CONVERTER.convert(expiration);
}
/**
* Converts a given {@link SetOption} to the according {@code SET} command argument.<br />
* <dl>
* <dt>{@link SetOption#UPSERT}</dt>
* <dd>{@code byte[0]}</dd>
* <dt>{@link SetOption#SET_IF_ABSENT}</dt>
* <dd>{@code NX}</dd>
* <dt>{@link SetOption#SET_IF_PRESENT}</dt>
* <dd>{@code XX}</dd>
* </dl>
*
* @param option
* @return
* @since 1.7
*/
public static byte[] toSetCommandNxXxArgument(SetOption option) {
return SET_OPTION_TO_COMMAND_OPTION_CONVERTER.convert(option);
}
private static byte[] boundaryToBytes(Boundary boundary, byte[] inclPrefix, byte[] exclPrefix) {
byte[] prefix = boundary.isIncluding() ? inclPrefix : exclPrefix;
byte[] value = null;
if (boundary.getValue() instanceof byte[]) {
value = (byte[]) boundary.getValue();
} else if (boundary.getValue() instanceof Double) {
value = toBytes((Double) boundary.getValue());
} else if (boundary.getValue() instanceof Long) {
value = toBytes((Long) boundary.getValue());
} else if (boundary.getValue() instanceof Integer) {
value = toBytes((Integer) boundary.getValue());
} else if (boundary.getValue() instanceof String) {
value = toBytes((String) boundary.getValue());
} else {
throw new IllegalArgumentException(String.format("Cannot convert %s to binary format", boundary.getValue()));
}
ByteBuffer buffer = ByteBuffer.allocate(prefix.length + value.length);
buffer.put(prefix);
buffer.put(value);
return buffer.array();
}
/**
* Convert {@link ScanOptions} to Jedis {@link ScanParams}.
*
* @param options
* @return
*/
public static ScanParams toScanParams(ScanOptions options) {
ScanParams sp = new ScanParams();
if (!options.equals(ScanOptions.NONE)) {
if (options.getCount() != null) {
sp.count(options.getCount().intValue());
}
if (StringUtils.hasText(options.getPattern())) {
sp.match(options.getPattern());
}
}
return sp;
}
static Converter<List<String>, Long> toTimeConverter() {
return STRING_LIST_TO_TIME_CONVERTER;
}
/**
* @param source
* @return
* @since 1.8
*/
public static List<String> toStrings(List<byte[]> source) {
return BYTES_LIST_TO_STRING_LIST_CONVERTER.convert(source);
}
/**
* @return
*/
public static ListConverter<byte[], String> bytesListToStringListConverter() {
return BYTES_LIST_TO_STRING_LIST_CONVERTER;
}
/**
* @return
* @since 1.8
*/
public static ListConverter<redis.clients.jedis.GeoCoordinate, Point> geoCoordinateToPointConverter() {
return LIST_GEO_COORDINATE_TO_POINT_CONVERTER;
}
/**
* Get a {@link Converter} capable of converting {@link GeoRadiusResponse} into {@link GeoResults}.
*
* @param metric
* @return
* @since 1.8
*/
public static Converter<List<redis.clients.jedis.GeoRadiusResponse>, GeoResults<GeoLocation<byte[]>>> geoRadiusResponseToGeoResultsConverter(
Metric metric) {
return GeoResultsConverterFactory.INSTANCE.forMetric(metric);
}
/**
* Convert {@link Metric} into {@link GeoUnit}.
*
* @param metric
* @return
* @since 1.8
*/
public static GeoUnit toGeoUnit(Metric metric) {
Metric metricToUse = metric == null || ObjectUtils.nullSafeEquals(Metrics.NEUTRAL, metric) ? DistanceUnit.METERS
: metric;
return ObjectUtils.caseInsensitiveValueOf(GeoUnit.values(), metricToUse.getAbbreviation());
}
/**
* Convert {@link Point} into {@link GeoCoordinate}.
*
* @param source
* @return
* @since 1.8
*/
public static GeoCoordinate toGeoCoordinate(Point source) {
return source == null ? null : new redis.clients.jedis.GeoCoordinate(source.getX(), source.getY());
}
/**
* Convert {@link GeoRadiusCommandArgs} into {@link GeoRadiusParam}.
*
* @param source
* @return
* @since 1.8
*/
public static GeoRadiusParam toGeoRadiusParam(GeoRadiusCommandArgs source) {
GeoRadiusParam param = GeoRadiusParam.geoRadiusParam();
if (source == null) {
return param;
}
if (source.hasFlags()) {
for (Flag flag : source.getFlags()) {
switch (flag) {
case WITHCOORD:
param.withCoord();
break;
case WITHDIST:
param.withDist();
break;
}
}
}
if (source.hasSortDirection()) {
switch (source.getSortDirection()) {
case ASC:
param.sortAscending();
break;
case DESC:
param.sortDescending();
break;
}
}
if (source.hasLimit()) {
param.count(source.getLimit().intValue());
}
return param;
}
/**
* @author Christoph Strobl
* @since 1.8
*/
static enum GeoResultsConverterFactory {
INSTANCE;
Converter<List<redis.clients.jedis.GeoRadiusResponse>, GeoResults<GeoLocation<byte[]>>> forMetric(Metric metric) {
return new GeoResultsConverter(
metric == null || ObjectUtils.nullSafeEquals(Metrics.NEUTRAL, metric) ? DistanceUnit.METERS : metric);
}
private static class GeoResultsConverter
implements Converter<List<redis.clients.jedis.GeoRadiusResponse>, GeoResults<GeoLocation<byte[]>>> {
private Metric metric;
public GeoResultsConverter(Metric metric) {
this.metric = metric;
}
@Override
public GeoResults<GeoLocation<byte[]>> convert(List<GeoRadiusResponse> source) {
List<GeoResult<GeoLocation<byte[]>>> results = new ArrayList<>(source.size());
Converter<redis.clients.jedis.GeoRadiusResponse, GeoResult<GeoLocation<byte[]>>> converter = GeoResultConverterFactory.INSTANCE
.forMetric(metric);
for (GeoRadiusResponse result : source) {
results.add(converter.convert(result));
}
return new GeoResults<>(results, metric);
}
}
}
/**
* @author Christoph Strobl
* @since 1.8
*/
static enum GeoResultConverterFactory {
INSTANCE;
Converter<redis.clients.jedis.GeoRadiusResponse, GeoResult<GeoLocation<byte[]>>> forMetric(Metric metric) {
return new GeoResultConverter(metric);
}
private static class GeoResultConverter
implements Converter<redis.clients.jedis.GeoRadiusResponse, GeoResult<GeoLocation<byte[]>>> {
private Metric metric;
public GeoResultConverter(Metric metric) {
this.metric = metric;
}
@Override
public GeoResult<GeoLocation<byte[]>> convert(redis.clients.jedis.GeoRadiusResponse source) {
Point point = GEO_COORDINATE_TO_POINT_CONVERTER.convert(source.getCoordinate());
return new GeoResult<>(new GeoLocation<>(source.getMember(), point),
new Distance(source.getDistance(), metric));
}
}
}
}