/* * Copyright 2013-2016 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.convert; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; 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.redis.connection.DataType; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.connection.RedisClusterNode.RedisClusterNodeBuilder; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisNode.NodeType; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import lombok.RequiredArgsConstructor; /** * Common type converters * * @author Jennifer Hickey * @author Thomas Darimont * @author Mark Paluch * @author Christoph Strobl */ abstract public class Converters { private static final byte[] ONE = new byte[] { '1' }; private static final byte[] ZERO = new byte[] { '0' }; private static final String CLUSTER_NODES_LINE_SEPARATOR = "\n"; private static final Converter<String, Properties> STRING_TO_PROPS = new StringToPropertiesConverter(); private static final Converter<Long, Boolean> LONG_TO_BOOLEAN = new LongToBooleanConverter(); private static final Converter<String, DataType> STRING_TO_DATA_TYPE = new StringToDataTypeConverter(); private static final Converter<Map<?, ?>, Properties> MAP_TO_PROPERTIES = MapToPropertiesConverter.INSTANCE; private static final Converter<String, RedisClusterNode> STRING_TO_CLUSTER_NODE_CONVERTER; private static final Map<String, Flag> flagLookupMap; static { flagLookupMap = new LinkedHashMap<String, RedisClusterNode.Flag>(Flag.values().length, 1); for (Flag flag : Flag.values()) { flagLookupMap.put(flag.getRaw(), flag); } STRING_TO_CLUSTER_NODE_CONVERTER = new Converter<String, RedisClusterNode>() { static final int ID_INDEX = 0; static final int HOST_PORT_INDEX = 1; static final int FLAGS_INDEX = 2; static final int MASTER_ID_INDEX = 3; static final int LINK_STATE_INDEX = 7; static final int SLOTS_INDEX = 8; @Override public RedisClusterNode convert(String source) { String[] args = source.split(" "); String[] hostAndPort = StringUtils.split(args[HOST_PORT_INDEX], ":"); SlotRange range = parseSlotRange(args); Set<Flag> flags = parseFlags(args); String portPart = hostAndPort[1]; if (portPart.contains("@")) { portPart = portPart.substring(0, portPart.indexOf('@')); } RedisClusterNodeBuilder nodeBuilder = RedisClusterNode.newRedisClusterNode() .listeningAt(hostAndPort[0], Integer.valueOf(portPart)) // .withId(args[ID_INDEX]) // .promotedAs(flags.contains(Flag.MASTER) ? NodeType.MASTER : NodeType.SLAVE) // .serving(range) // .withFlags(flags) // .linkState(parseLinkState(args)); if (!args[MASTER_ID_INDEX].isEmpty() && !args[MASTER_ID_INDEX].startsWith("-")) { nodeBuilder.slaveOf(args[MASTER_ID_INDEX]); } return nodeBuilder.build(); } private Set<Flag> parseFlags(String[] args) { String raw = args[FLAGS_INDEX]; Set<Flag> flags = new LinkedHashSet<RedisClusterNode.Flag>(8, 1); if (StringUtils.hasText(raw)) { for (String flag : raw.split(",")) { flags.add(flagLookupMap.get(flag)); } } return flags; } private LinkState parseLinkState(String[] args) { String raw = args[LINK_STATE_INDEX]; if (StringUtils.hasText(raw)) { return LinkState.valueOf(raw.toUpperCase()); } return LinkState.DISCONNECTED; } private SlotRange parseSlotRange(String[] args) { Set<Integer> slots = new LinkedHashSet<Integer>(); for (int i = SLOTS_INDEX; i < args.length; i++) { String raw = args[i]; if (raw.startsWith("[")) { continue; } if (raw.contains("-")) { String[] slotRange = StringUtils.split(raw, "-"); if (slotRange != null) { int from = Integer.valueOf(slotRange[0]); int to = Integer.valueOf(slotRange[1]); for (int slot = from; slot <= to; slot++) { slots.add(slot); } } } else { slots.add(Integer.valueOf(raw)); } } SlotRange range = new SlotRange(slots); return range; } }; } public static Boolean stringToBoolean(String s) { return ObjectUtils.nullSafeEquals("OK", s); } public static Converter<String, Properties> stringToProps() { return STRING_TO_PROPS; } public static Converter<Long, Boolean> longToBoolean() { return LONG_TO_BOOLEAN; } public static Converter<String, DataType> stringToDataType() { return STRING_TO_DATA_TYPE; } public static Properties toProperties(String source) { return STRING_TO_PROPS.convert(source); } public static Properties toProperties(Map<?, ?> source) { return MAP_TO_PROPERTIES.convert(source); } public static Boolean toBoolean(Long source) { return LONG_TO_BOOLEAN.convert(source); } public static DataType toDataType(String source) { return STRING_TO_DATA_TYPE.convert(source); } public static byte[] toBit(Boolean source) { return (source ? ONE : ZERO); } /** * Converts the result of a single line of {@code CLUSTER NODES} into a {@link RedisClusterNode}. * * @param clusterNodesLine * @return * @since 1.7 */ protected static RedisClusterNode toClusterNode(String clusterNodesLine) { return STRING_TO_CLUSTER_NODE_CONVERTER.convert(clusterNodesLine); } /** * Converts lines from the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s. * * @param clusterNodes * @return * @since 1.7 */ public static Set<RedisClusterNode> toSetOfRedisClusterNodes(Collection<String> lines) { if (CollectionUtils.isEmpty(lines)) { return Collections.emptySet(); } Set<RedisClusterNode> nodes = new LinkedHashSet<RedisClusterNode>(lines.size()); for (String line : lines) { nodes.add(toClusterNode(line)); } return nodes; } /** * Converts the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s. * * @param clusterNodes * @return * @since 1.7 */ public static Set<RedisClusterNode> toSetOfRedisClusterNodes(String clusterNodes) { if (StringUtils.isEmpty(clusterNodes)) { return Collections.emptySet(); } String[] lines = clusterNodes.split(CLUSTER_NODES_LINE_SEPARATOR); return toSetOfRedisClusterNodes(Arrays.asList(lines)); } public static List<Object> toObjects(Set<Tuple> tuples) { List<Object> tupleArgs = new ArrayList<Object>(tuples.size() * 2); for (Tuple tuple : tuples) { tupleArgs.add(tuple.getScore()); tupleArgs.add(tuple.getValue()); } return tupleArgs; } /** * Returns the timestamp constructed from the given {@code seconds} and {@code microseconds}. * * @param seconds server time in seconds * @param microseconds elapsed microseconds in current second * @return */ public static Long toTimeMillis(String seconds, String microseconds) { return NumberUtils.parseNumber(seconds, Long.class) * 1000L + NumberUtils.parseNumber(microseconds, Long.class) / 1000L; } /** * Converts {@code seconds} to the given {@link TimeUnit}. * * @param seconds * @param targetUnit must not be {@literal null}. * @return * @since 1.8 */ public static long secondsToTimeUnit(long seconds, TimeUnit targetUnit) { Assert.notNull(targetUnit, "TimeUnit must not be null!"); if (seconds > 0) { return targetUnit.convert(seconds, TimeUnit.SECONDS); } return seconds; } /** * Creates a new {@link Converter} to convert from seconds to the given {@link TimeUnit}. * * @param timeUnit muist not be {@literal null}. * @return * @since 1.8 */ public static Converter<Long, Long> secondsToTimeUnit(final TimeUnit timeUnit) { return new Converter<Long, Long>() { @Override public Long convert(Long seconds) { return secondsToTimeUnit(seconds, timeUnit); } }; } /** * Converts {@code milliseconds} to the given {@link TimeUnit}. * * @param milliseconds * @param targetUnit must not be {@literal null}. * @return * @since 1.8 */ public static long millisecondsToTimeUnit(long milliseconds, TimeUnit targetUnit) { Assert.notNull(targetUnit, "TimeUnit must not be null!"); if (milliseconds > 0) { return targetUnit.convert(milliseconds, TimeUnit.MILLISECONDS); } return milliseconds; } /** * Creates a new {@link Converter} to convert from milliseconds to the given {@link TimeUnit}. * * @param timeUnit muist not be {@literal null}. * @return * @since 1.8 */ public static Converter<Long, Long> millisecondsToTimeUnit(final TimeUnit timeUnit) { return new Converter<Long, Long>() { @Override public Long convert(Long seconds) { return millisecondsToTimeUnit(seconds, timeUnit); } }; } /** * {@link Converter} capable of deserializing {@link GeoResults}. * * @param serializer * @return * @since 1.8 */ public static <V> Converter<GeoResults<GeoLocation<byte[]>>, GeoResults<GeoLocation<V>>> deserializingGeoResultsConverter( RedisSerializer<V> serializer) { return new DeserializingGeoResultsConverter<V>(serializer); } /** * {@link Converter} capable of converting Double into {@link Distance} using given {@link Metric}. * * @param metric * @return * @since 1.8 */ public static Converter<Double, Distance> distanceConverterForMetric(Metric metric) { return DistanceConverterFactory.INSTANCE.forMetric(metric); } /** * @author Christoph Strobl * @since 1.8 */ enum DistanceConverterFactory { INSTANCE; DistanceConverter forMetric(Metric metric) { return new DistanceConverter( metric == null || ObjectUtils.nullSafeEquals(Metrics.NEUTRAL, metric) ? DistanceUnit.METERS : metric); } static class DistanceConverter implements Converter<Double, Distance> { private Metric metric; public DistanceConverter(Metric metric) { this.metric = metric == null || ObjectUtils.nullSafeEquals(Metrics.NEUTRAL, metric) ? DistanceUnit.METERS : metric; } @Override public Distance convert(Double source) { return source == null ? null : new Distance(source, metric); } } } /** * @author Christoph Strobl * @param <V> * @since 1.8 */ @RequiredArgsConstructor static class DeserializingGeoResultsConverter<V> implements Converter<GeoResults<GeoLocation<byte[]>>, GeoResults<GeoLocation<V>>> { final RedisSerializer<V> serializer; @Override public GeoResults<GeoLocation<V>> convert(GeoResults<GeoLocation<byte[]>> source) { if (source == null) { return new GeoResults<GeoLocation<V>>(Collections.<GeoResult<GeoLocation<V>>> emptyList()); } List<GeoResult<GeoLocation<V>>> values = new ArrayList<GeoResult<GeoLocation<V>>>(source.getContent().size()); for (GeoResult<GeoLocation<byte[]>> value : source.getContent()) { values.add(new GeoResult<GeoLocation<V>>( new GeoLocation<V>(serializer.deserialize(value.getContent().getName()), value.getContent().getPoint()), value.getDistance())); } return new GeoResults<GeoLocation<V>>(values, source.getAverageDistance().getMetric()); } } }