From 5de73903c1272bdfdd6fdcb828463b8f2eaa6ec8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Dec 2017 10:57:50 +0100 Subject: [PATCH] DATAREDIS-744 - Support Redis hashes with colon in their id. We now support Redis hashes via Repository support that contain colon in their id. We're using colons to split a composite id string into keyspace and id parts. Previously, we partially rejected processing of id's that don't exactly match the number of parts delimited by colon. This caused leftovers in secondary indexes. Original Pull Request: #298 --- .../data/redis/core/RedisKeyExpiredEvent.java | 21 +- .../data/redis/core/RedisKeyValueAdapter.java | 14 +- .../core/convert/MappingRedisConverter.java | 208 +++++++++++++++++- .../core/RedisKeyExpiredEventUnitTests.java | 46 ++++ .../redis/core/RedisKeyValueAdapterTests.java | 56 +++-- .../BinaryKeyspaceIdentifierUnitTests.java | 67 ++++++ .../convert/KeyspaceIdentifierUnitTests.java | 65 ++++++ 7 files changed, 434 insertions(+), 43 deletions(-) create mode 100644 src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java index 8b2119b36..299f47727 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java @@ -19,7 +19,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.springframework.context.ApplicationEvent; -import org.springframework.data.redis.util.ByteUtils; +import org.springframework.data.redis.core.convert.MappingRedisConverter.BinaryKeyspaceIdentifier; import org.springframework.lang.Nullable; /** @@ -27,6 +27,7 @@ import org.springframework.lang.Nullable; * expires. It might but must not hold the expired value itself next to the key. * * @author Christoph Strobl + * @author Mark Paluch * @since 1.7 */ public class RedisKeyExpiredEvent extends RedisKeyspaceEvent { @@ -36,7 +37,7 @@ public class RedisKeyExpiredEvent extends RedisKeyspaceEvent { */ public static final Charset CHARSET = StandardCharsets.UTF_8; - private final byte[][] args; + private final BinaryKeyspaceIdentifier objectId; private final @Nullable Object value; /** @@ -69,7 +70,12 @@ public class RedisKeyExpiredEvent extends RedisKeyspaceEvent { public RedisKeyExpiredEvent(@Nullable String channel, byte[] key, @Nullable Object value) { super(channel, key); - args = ByteUtils.split(key, ':'); + if (BinaryKeyspaceIdentifier.isValid(key)) { + this.objectId = BinaryKeyspaceIdentifier.of(key); + } else { + this.objectId = null; + } + this.value = value; } @@ -79,12 +85,7 @@ public class RedisKeyExpiredEvent extends RedisKeyspaceEvent { * @return {@literal null} if it could not be determined. */ public String getKeyspace() { - - if (args.length >= 2) { - return new String(args[0], CHARSET); - } - - return null; + return objectId != null ? new String(objectId.getKeyspace(), CHARSET) : null; } /** @@ -93,7 +94,7 @@ public class RedisKeyExpiredEvent extends RedisKeyspaceEvent { * @return */ public byte[] getId() { - return args.length == 2 ? args[1] : args[0]; + return objectId != null ? objectId.getId() : getSource(); } /** diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index 07dda1706..bebf53c26 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -48,6 +48,8 @@ import org.springframework.data.redis.core.convert.CustomConversions; import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.convert.MappingRedisConverter.BinaryKeyspaceIdentifier; +import org.springframework.data.redis.core.convert.MappingRedisConverter.KeyspaceIdentifier; import org.springframework.data.redis.core.convert.PathIndexResolver; import org.springframework.data.redis.core.convert.RedisConverter; import org.springframework.data.redis.core.convert.RedisCustomConversions; @@ -240,7 +242,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter connection.expire(objectKey, rdo.getTimeToLive()); // add phantom key so values can be restored - byte[] phantomKey = ByteUtils.concat(objectKey, toBytes(":phantom")); + byte[] phantomKey = ByteUtils.concat(objectKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX); connection.del(phantomKey); connection.hMSet(phantomKey, rdo.getBucket().rawMap()); connection.expire(phantomKey, rdo.getTimeToLive() + 300); @@ -471,14 +473,14 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter connection.expire(redisKey, rdo.getTimeToLive()); // add phantom key so values can be restored - byte[] phantomKey = ByteUtils.concat(redisKey, toBytes(":phantom")); + byte[] phantomKey = ByteUtils.concat(redisKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX); connection.hMSet(phantomKey, rdo.getBucket().rawMap()); connection.expire(phantomKey, rdo.getTimeToLive() + 300); } else { connection.persist(redisKey); - connection.persist(ByteUtils.concat(redisKey, toBytes(":phantom"))); + connection.persist(ByteUtils.concat(redisKey, BinaryKeyspaceIdentifier.PHANTOM_SUFFIX)); } } @@ -763,7 +765,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter byte[] key = message.getBody(); - byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService().convert(":phantom", byte[].class)); + byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService().convert(KeyspaceIdentifier.PHANTOM_SUFFIX, byte[].class)); Map hash = ops.execute((RedisCallback>) connection -> { @@ -799,9 +801,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter return false; } - byte[][] args = ByteUtils.split(message.getBody(), ':'); - - return args.length == 2; + return BinaryKeyspaceIdentifier.isValid(message.getBody()); } } diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index 1d4452cab..0b65db051 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -16,6 +16,9 @@ package org.springframework.data.redis.core.convert; import lombok.RequiredArgsConstructor; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; import java.lang.reflect.Array; import java.util.*; @@ -52,6 +55,7 @@ import org.springframework.data.redis.core.index.Indexed; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.redis.core.mapping.RedisPersistentEntity; import org.springframework.data.redis.core.mapping.RedisPersistentProperty; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -308,9 +312,14 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { for (Entry entry : bucket.entrySet()) { String referenceKey = fromBytes(entry.getValue(), String.class); - String[] args = referenceKey.split(":"); - Map rawHash = referenceResolver.resolveReference(args[1], args[0]); + if (!KeyspaceIdentifier.isValid(referenceKey)) { + continue; + } + + KeyspaceIdentifier identifier = KeyspaceIdentifier.of(referenceKey); + Map rawHash = referenceResolver.resolveReference(identifier.getId(), + identifier.getKeyspace()); if (!CollectionUtils.isEmpty(rawHash)) { target.add(read(association.getInverse().getActualType(), new RedisData(rawHash))); @@ -326,15 +335,18 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { return; } - String key = fromBytes(binKey, String.class); + String referenceKey = fromBytes(binKey, String.class); + if (KeyspaceIdentifier.isValid(referenceKey)) { - String[] args = key.split(":"); + KeyspaceIdentifier identifier = KeyspaceIdentifier.of(referenceKey); - Map rawHash = referenceResolver.resolveReference(args[1], args[0]); + Map rawHash = referenceResolver.resolveReference(identifier.getId(), + identifier.getKeyspace()); - if (!CollectionUtils.isEmpty(rawHash)) { - accessor.setProperty(association.getInverse(), - read(association.getInverse().getActualType(), new RedisData(rawHash))); + if (!CollectionUtils.isEmpty(rawHash)) { + accessor.setProperty(association.getInverse(), + read(association.getInverse().getActualType(), new RedisData(rawHash))); + } } } }); @@ -434,9 +446,10 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { targetProperty = getTargetPropertyOrNullForPath(path.replaceAll("\\.\\[.*\\]", ""), update.getTarget()); TypeInformation ti = targetProperty == null ? ClassTypeInformation.OBJECT - : (targetProperty.isMap() ? (targetProperty.getTypeInformation().getMapValueType() != null - ? targetProperty.getTypeInformation().getRequiredMapValueType() - : ClassTypeInformation.OBJECT) : targetProperty.getTypeInformation().getActualType()); + : (targetProperty.isMap() + ? (targetProperty.getTypeInformation().getMapValueType() != null + ? targetProperty.getTypeInformation().getRequiredMapValueType() : ClassTypeInformation.OBJECT) + : targetProperty.getTypeInformation().getActualType()); writeInternal(entity.getKeySpace(), pUpdate.getPropertyPath(), pUpdate.getValue(), ti, sink); return; @@ -1160,4 +1173,177 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { } } + /** + * Value object representing a Redis Hash/Object identifier composed from keyspace and object id in the form of + * {@literal keyspace:id}. + * + * @author Mark Paluch + * @since 1.8.10 + */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + public static class KeyspaceIdentifier { + + public static final String PHANTOM = "phantom"; + public static final String DELIMITTER = ":"; + public static final String PHANTOM_SUFFIX = DELIMITTER + PHANTOM; + + private String keyspace; + private String id; + private boolean phantomKey; + + /** + * Parse a {@code key} into {@link KeyspaceIdentifier}. + * + * @param key the key representation. + * @return {@link BinaryKeyspaceIdentifier} for binary key. + */ + public static KeyspaceIdentifier of(String key) { + + Assert.isTrue(isValid(key), String.format("Invalid key %s", key)); + + boolean phantomKey = key.endsWith(PHANTOM_SUFFIX); + int keyspaceEndIndex = key.indexOf(DELIMITTER); + String keyspace = key.substring(0, keyspaceEndIndex); + String id; + + if (phantomKey) { + id = key.substring(keyspaceEndIndex + 1, key.length() - PHANTOM_SUFFIX.length()); + } else { + id = key.substring(keyspaceEndIndex + 1); + } + + return new KeyspaceIdentifier(keyspace, id, phantomKey); + } + + /** + * Check whether the {@code key} is valid, in particular whether the key contains a keyspace and an id part in the + * form of {@literal keyspace:id}. + * + * @param key the key. + * @return {@literal true} if the key is valid. + */ + public static boolean isValid(String key) { + + if (key == null) { + return false; + } + + int keyspaceEndIndex = key.indexOf(DELIMITTER); + + return keyspaceEndIndex > 0 && key.length() > keyspaceEndIndex; + } + } + + /** + * Value object representing a binary Redis Hash/Object identifier composed from keyspace and object id in the form of + * {@literal keyspace:id}. + * + * @author Mark Paluch + * @since 1.8.10 + */ + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + public static class BinaryKeyspaceIdentifier { + + public static final byte[] PHANTOM = KeyspaceIdentifier.PHANTOM.getBytes(); + public static final byte DELIMITTER = ':'; + public static final byte[] PHANTOM_SUFFIX = ByteUtils.concat(new byte[] { DELIMITTER }, PHANTOM); + + private byte[] keyspace; + private byte[] id; + private boolean phantomKey; + + /** + * Parse a binary {@code key} into {@link BinaryKeyspaceIdentifier}. + * + * @param key the binary key representation. + * @return {@link BinaryKeyspaceIdentifier} for binary key. + */ + public static BinaryKeyspaceIdentifier of(byte[] key) { + + Assert.isTrue(isValid(key), String.format("Invalid key %s", new String(key))); + + boolean phantomKey = startsWith(key, PHANTOM_SUFFIX, key.length - PHANTOM_SUFFIX.length); + + int keyspaceEndIndex = find(key, DELIMITTER); + byte[] keyspace = getKeyspace(key, keyspaceEndIndex); + byte[] id = getId(key, phantomKey, keyspaceEndIndex); + + return new BinaryKeyspaceIdentifier(keyspace, id, phantomKey); + } + + /** + * Check whether the {@code key} is valid, in particular whether the key contains a keyspace and an id part in the + * form of {@literal keyspace:id}. + * + * @param key the key. + * @return {@literal true} if the key is valid. + */ + public static boolean isValid(byte[] key) { + + if (key == null) { + return false; + } + + int keyspaceEndIndex = find(key, DELIMITTER); + + return keyspaceEndIndex > 0 && key.length > keyspaceEndIndex; + } + + private static byte[] getId(byte[] key, boolean phantomKey, int keyspaceEndIndex) { + + int idSize; + + if (phantomKey) { + idSize = (key.length - PHANTOM_SUFFIX.length) - (keyspaceEndIndex + 1); + } else { + + idSize = key.length - (keyspaceEndIndex + 1); + } + + byte[] id = new byte[idSize]; + System.arraycopy(key, keyspaceEndIndex + 1, id, 0, idSize); + + return id; + } + + private static byte[] getKeyspace(byte[] key, int keyspaceEndIndex) { + + byte[] keyspace = new byte[keyspaceEndIndex]; + System.arraycopy(key, 0, keyspace, 0, keyspaceEndIndex); + + return keyspace; + } + + private static boolean startsWith(byte[] haystack, byte[] prefix, int offset) { + + int to = offset; + int prefixOffset = 0; + int prefixLength = prefix.length; + + if ((offset < 0) || (offset > haystack.length - prefixLength)) { + return false; + } + + while (--prefixLength >= 0) { + if (haystack[to++] != prefix[prefixOffset++]) { + return false; + } + } + + return true; + } + + private static int find(byte[] haystack, byte needle) { + + for (int i = 0; i < haystack.length; i++) { + if (haystack[i] == needle) { + return i; + } + } + + return -1; + } + } } diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java new file mode 100644 index 000000000..3f4cc27bd --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyExpiredEventUnitTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 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.core; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link RedisKeyExpiredEvent}. + * + * @author Mark Paluch + */ +public class RedisKeyExpiredEventUnitTests { + + @Test // DATAREDIS-744 + public void shouldReturnKeyspace() { + + assertThat(new RedisKeyExpiredEvent("foo".getBytes(), "").getKeyspace(), is(nullValue())); + assertThat(new RedisKeyExpiredEvent("foo:bar".getBytes(), "").getKeyspace(), is(equalTo("foo"))); + assertThat(new RedisKeyExpiredEvent("foo:bar:baz".getBytes(), "").getKeyspace(), is(equalTo("foo"))); + } + + @Test // DATAREDIS-744 + public void shouldReturnId() { + + assertThat(new RedisKeyExpiredEvent("foo".getBytes(), "").getId(), is(equalTo("foo".getBytes()))); + assertThat(new RedisKeyExpiredEvent("foo:bar".getBytes(), "").getId(), is(equalTo("bar".getBytes()))); + assertThat(new RedisKeyExpiredEvent("foo:bar:baz".getBytes(), "").getId(), + is(equalTo("bar:baz".getBytes()))); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java index a2fe3ac88..e24699bf1 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterTests.java @@ -146,6 +146,20 @@ public class RedisKeyValueAdapterTests { assertThat(template.opsForHash().entries("persons:1").size(), is(2)); } + @Test // DATAREDIS-744 + public void putWritesDataWithColonCorrectly() { + + Person rand = new Person(); + rand.age = 24; + + adapter.put("1:a", rand, "persons"); + + assertThat(template.keys("persons*"), hasItems("persons", "persons:1:a")); + assertThat(template.opsForSet().size("persons"), is(1L)); + assertThat(template.opsForSet().members("persons"), hasItems("1:a")); + assertThat(template.opsForHash().entries("persons:1:a").size(), is(2)); + } + @Test // DATAREDIS-425 public void putWritesSimpleIndexDataCorrectly() { @@ -158,6 +172,18 @@ public class RedisKeyValueAdapterTests { assertThat(template.opsForSet().members("persons:firstname:rand"), hasItems("1")); } + @Test // DATAREDIS-744 + public void putWritesSimpleIndexDataWithColonCorrectly() { + + Person rand = new Person(); + rand.firstname = "rand"; + + adapter.put("1:a", rand, "persons"); + + assertThat(template.keys("persons*"), hasItem("persons:firstname:rand")); + assertThat(template.opsForSet().members("persons:firstname:rand"), hasItems("1:a")); + } + @Test // DATAREDIS-425 public void putWritesNestedDataCorrectly() { @@ -184,15 +210,15 @@ public class RedisKeyValueAdapterTests { assertThat(template.opsForSet().members("persons:address.country:Andor"), hasItems("1")); } - @Test // DATAREDIS-425 + @Test // DATAREDIS-425, DATAREDIS-744 public void getShouldReadSimpleObjectCorrectly() { Map map = new LinkedHashMap<>(); map.put("_class", Person.class.getName()); map.put("age", "24"); - template.opsForHash().putAll("persons:load-1", map); + template.opsForHash().putAll("persons:load-1:a", map); - Object loaded = adapter.get("load-1", "persons"); + Object loaded = adapter.get("load-1:a", "persons"); assertThat(loaded, instanceOf(Person.class)); assertThat(((Person) loaded).age, is(24)); @@ -257,7 +283,7 @@ public class RedisKeyValueAdapterTests { assertThat(template.opsForSet().members("persons:firstname:rand"), not(hasItem("1"))); } - @Test // DATAREDIS-425 + @Test // DATAREDIS-425, DATAREDIS-744 public void keyExpiredEventShouldRemoveHelperStructures() throws Exception { assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true")); @@ -267,22 +293,22 @@ public class RedisKeyValueAdapterTests { map.put("firstname", "rand"); map.put("address.country", "Andor"); - template.opsForHash().putAll("persons:1", map); + template.opsForHash().putAll("persons:1:b", map); template.opsForSet().add("persons", "1"); - template.opsForSet().add("persons:firstname:rand", "1"); - template.opsForSet().add("persons:1:idx", "persons:firstname:rand"); + template.opsForSet().add("persons:firstname:rand", "1:b"); + template.opsForSet().add("persons:1:b:idx", "persons:firstname:rand"); - template.expire("persons:1", 100, TimeUnit.MILLISECONDS); + template.expire("persons:1:b", 100, TimeUnit.MILLISECONDS); - waitUntilKeyIsGone(template, "persons:1"); - waitUntilKeyIsGone(template, "persons:1:phantom"); + waitUntilKeyIsGone(template, "persons:1:b"); + waitUntilKeyIsGone(template, "persons:1:b:phantom"); waitUntilKeyIsGone(template, "persons:firstname:rand"); assertThat(template.hasKey("persons:1"), is(false)); assertThat(template.hasKey("persons:firstname:rand"), is(false)); - assertThat(template.hasKey("persons:1:idx"), is(false)); - assertThat(template.opsForSet().members("persons"), not(hasItem("1"))); + assertThat(template.hasKey("persons:1:b:idx"), is(false)); + assertThat(template.opsForSet().members("persons"), not(hasItem("1:b"))); } @Test // DATAREDIS-589 @@ -348,17 +374,17 @@ public class RedisKeyValueAdapterTests { assertThat(template.opsForSet().isMember("persons:mat:idx", "persons:firstname:mat"), is(true)); } - @Test // DATAREDIS-471 + @Test // DATAREDIS-471, DATAREDIS-744 public void updateShouldAlterIndexDataCorrectly() { Person rand = new Person(); rand.firstname = "rand"; - adapter.put("1", rand, "persons"); + adapter.put("1:a", rand, "persons"); assertThat(template.hasKey("persons:firstname:rand"), is(true)); - PartialUpdate update = new PartialUpdate<>("1", Person.class) // + PartialUpdate update = new PartialUpdate<>("1:a", Person.class) // .set("firstname", "mat"); adapter.update(update); diff --git a/src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java new file mode 100644 index 000000000..821d93794 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/BinaryKeyspaceIdentifierUnitTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 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.core.convert; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.redis.core.convert.MappingRedisConverter.BinaryKeyspaceIdentifier; + +/** + * Unit tests for {@link BinaryKeyspaceIdentifier}. + * + * @author Mark Paluch + */ +public class BinaryKeyspaceIdentifierUnitTests { + + @Test // DATAREDIS-744 + public void shouldReturnIfKeyIsValid() { + + assertThat(BinaryKeyspaceIdentifier.isValid(null), is(false)); + assertThat(BinaryKeyspaceIdentifier.isValid("foo".getBytes()), is(false)); + assertThat(BinaryKeyspaceIdentifier.isValid("".getBytes()), is(false)); + assertThat(BinaryKeyspaceIdentifier.isValid("foo:bar".getBytes()), is(true)); + assertThat(BinaryKeyspaceIdentifier.isValid("foo:bar:baz".getBytes()), is(true)); + assertThat(BinaryKeyspaceIdentifier.isValid("foo:bar:baz:phantom".getBytes()), is(true)); + } + + @Test // DATAREDIS-744 + public void shouldReturnKeyspace() { + + assertThat(BinaryKeyspaceIdentifier.of("foo:bar".getBytes()).getKeyspace(), is(equalTo("foo".getBytes()))); + assertThat(BinaryKeyspaceIdentifier.of("foo:bar:baz".getBytes()).getKeyspace(), is(equalTo("foo".getBytes()))); + assertThat(BinaryKeyspaceIdentifier.of("foo:bar:baz:phantom".getBytes()).getKeyspace(), + is(equalTo("foo".getBytes()))); + } + + @Test // DATAREDIS-744 + public void shouldReturnId() { + + assertThat(BinaryKeyspaceIdentifier.of("foo:bar".getBytes()).getId(), is(equalTo("bar".getBytes()))); + assertThat(BinaryKeyspaceIdentifier.of("foo:bar:baz".getBytes()).getId(), is(equalTo("bar:baz".getBytes()))); + assertThat(BinaryKeyspaceIdentifier.of("foo:bar:baz:phantom".getBytes()).getId(), + is(equalTo("bar:baz".getBytes()))); + } + + @Test // DATAREDIS-744 + public void shouldReturnPhantomKey() { + + assertThat(BinaryKeyspaceIdentifier.of("foo:bar".getBytes()).isPhantomKey(), is(false)); + assertThat(BinaryKeyspaceIdentifier.of("foo:bar:baz".getBytes()).isPhantomKey(), is(false)); + assertThat(BinaryKeyspaceIdentifier.of("foo:bar:baz:phantom".getBytes()).isPhantomKey(), is(true)); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java new file mode 100644 index 000000000..daac75a57 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/convert/KeyspaceIdentifierUnitTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 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.core.convert; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.redis.core.convert.MappingRedisConverter.KeyspaceIdentifier; + +/** + * Unit tests for {@link KeyspaceIdentifier}. + * + * @author Mark Paluch + */ +public class KeyspaceIdentifierUnitTests { + + @Test // DATAREDIS-744 + public void shouldReturnIfKeyIsValid() { + + assertThat(KeyspaceIdentifier.isValid(null), is(false)); + assertThat(KeyspaceIdentifier.isValid("foo"), is(false)); + assertThat(KeyspaceIdentifier.isValid(""), is(false)); + assertThat(KeyspaceIdentifier.isValid("foo:bar"), is(true)); + assertThat(KeyspaceIdentifier.isValid("foo:bar:baz"), is(true)); + assertThat(KeyspaceIdentifier.isValid("foo:bar:baz:phantom"), is(true)); + } + + @Test // DATAREDIS-744 + public void shouldReturnKeyspace() { + + assertThat(KeyspaceIdentifier.of("foo:bar").getKeyspace(), is("foo")); + assertThat(KeyspaceIdentifier.of("foo:bar:baz").getKeyspace(), is("foo")); + assertThat(KeyspaceIdentifier.of("foo:bar:baz:phantom").getKeyspace(), is("foo")); + } + + @Test // DATAREDIS-744 + public void shouldReturnId() { + + assertThat(KeyspaceIdentifier.of("foo:bar").getId(), is("bar")); + assertThat(KeyspaceIdentifier.of("foo:bar:baz").getId(), is("bar:baz")); + assertThat(KeyspaceIdentifier.of("foo:bar:baz:phantom").getId(), is("bar:baz")); + } + + @Test // DATAREDIS-744 + public void shouldReturnPhantomKey() { + + assertThat(KeyspaceIdentifier.of("foo:bar").isPhantomKey(), is(false)); + assertThat(KeyspaceIdentifier.of("foo:bar:baz").isPhantomKey(), is(false)); + assertThat(KeyspaceIdentifier.of("foo:bar:baz:phantom").isPhantomKey(), is(true)); + } +}