diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java new file mode 100644 index 0000000000..1d6aab05fd --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -0,0 +1,153 @@ +/* + * Copyright 2015 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.serializer; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.SerializerFactory; + +/** + * {@link RedisSerializer} that can read and write JSON for arbitrary input types at the expense of additional type + * information stored with with value by using Jackson's and Jackson Databind {@link ObjectMapper}. + *

+ * This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. + * Note:Null objects are serialized as empty arrays and vice versa. + *

+ * The serialized form consists of: + *

    + *
  1. Payload type name length (int, 4 bytes)
  2. + *
  3. Payload type name in bytes encoded with UTF-8
  4. + *
  5. Payload data bytes
  6. + *
+ * + * @author Thomas Darimont + * @since 1.6 + */ +public class GenericJackson2JsonRedisSerializer implements RedisSerializer { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Creates a new {@link GenericJackson2JsonRedisSerializer}. + */ + public GenericJackson2JsonRedisSerializer() {} + + /* (non-Javadoc) + * @see org.springframework.data.redis.serializer.RedisSerializer#deserialize(byte[]) + */ + @Override + public Object deserialize(byte[] bytes) throws SerializationException { + + if (SerializationUtils.isEmpty(bytes)) { + return null; + } + + try { + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + int typeLength = buffer.getInt(); + byte[] typeBytes = new byte[typeLength]; + buffer.get(typeBytes); + + // resolve valuetype first to allow early exit on unresolveable types + Class valueType = resolveTypeFrom(typeBytes); + + byte[] valueBytes = new byte[buffer.remaining()]; + buffer.get(valueBytes); + + return this.objectMapper.readValue(valueBytes, 0, valueBytes.length, valueType); + } catch (Exception ex) { + throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); + } + } + + /** + * Resolves the {@link Class} to be used from the given {@code typeBytes}. + *

+ * This can be overridden to cache type lookups. + * + * @param typeBytes + * @return + * @throws ClassNotFoundException + */ + protected Class resolveTypeFrom(byte[] typeBytes) throws ClassNotFoundException { + + String className = new String(typeBytes, DEFAULT_CHARSET); + return ClassUtils.resolveClassName(className, getClass().getClassLoader()); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.serializer.RedisSerializer#serialize(java.lang.Object) + */ + @Override + public byte[] serialize(Object value) throws SerializationException { + + if (value == null) { + return SerializationUtils.EMPTY_ARRAY; + } + + try { + + byte[] valueBytes = this.objectMapper.writeValueAsBytes(value); + byte[] typeBytes = convertTypeToBytes(value.getClass()); + + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + typeBytes.length + valueBytes.length); + buffer.putInt(typeBytes.length); + buffer.put(typeBytes); + buffer.put(valueBytes); + + return buffer.array(); + } catch (Exception ex) { + throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); + } + } + + /** + * Converts a given {@code Class} type to a {@code byte[]} representation. + *

+ * Note that the representation must be readable from {@link #resolveTypeFrom(byte[])}. + * + * @param type + * @return + */ + protected byte[] convertTypeToBytes(Class type) { + return type.getName().getBytes(DEFAULT_CHARSET); + } + + /** + * Sets the {@code ObjectMapper} for this view. If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} + * is used. + *

+ * Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON serialization + * process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for + * specific types. The other option for refining the serialization process is to use Jackson's provided annotations on + * the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + + Assert.notNull(objectMapper, "'objectMapper' must not be null"); + this.objectMapper = objectMapper; + } +} diff --git a/src/test/java/org/springframework/data/redis/AddressObjectFactory.java b/src/test/java/org/springframework/data/redis/AddressObjectFactory.java new file mode 100644 index 0000000000..f890892794 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/AddressObjectFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 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; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Thomas Darimont + */ +public class AddressObjectFactory implements ObjectFactory

{ + + private final AtomicInteger counter = new AtomicInteger(); + + @Override + public Address instance() { + return new Address("Street " + counter.intValue(), counter.intValue()); + } + +} diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerTests.java new file mode 100644 index 0000000000..cde3704464 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 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.serializer; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; + +import org.hamcrest.core.Is; +import org.hamcrest.core.IsNull; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.redis.AddressObjectFactory; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.Person; +import org.springframework.data.redis.PersonObjectFactory; + +/** + * @author Thomas Darimont + */ +public class GenericJackson2JsonRedisSerializerTests { + + private GenericJackson2JsonRedisSerializer serializer; + + private List> objectFactories; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + + this.serializer = new GenericJackson2JsonRedisSerializer(); + this.objectFactories = Arrays.> asList(new PersonObjectFactory(), + new AddressObjectFactory()); + } + + /** + * @see DATAREDIS-390 + */ + @Test + public void beAbleToSerializeMultipleTypes() throws Exception { + + for (ObjectFactory factory : objectFactories) { + Object instance = factory.instance(); + assertEquals(instance, serializer.deserialize(serializer.serialize(instance))); + } + } + + @Test + public void testJackson2JsonSerializerShouldReturnEmptyByteArrayWhenSerializingNull() { + assertThat(serializer.serialize(null), Is.is(new byte[0])); + } + + @Test + public void testJackson2JsonSerializerShouldReturnNullWhenDerserializingEmtyByteArray() { + assertThat(serializer.deserialize(new byte[0]), IsNull.nullValue()); + } + + @Test(expected = SerializationException.class) + public void testJackson2JsonSerilizerShouldThrowExceptionWhenDeserializingInvalidByteArray() { + + Person person = new PersonObjectFactory().instance(); + byte[] serializedValue = serializer.serialize(person); + Arrays.sort(serializedValue); // corrupt serialization result + + serializer.deserialize(serializedValue); + } + + @Test(expected = IllegalArgumentException.class) + public void testJackson2JsonSerilizerThrowsExceptionWhenSettingNullObjectMapper() { + serializer.setObjectMapper(null); + } + +} diff --git a/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java b/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java index 8b80cc7cfe..b76596b31e 100644 --- a/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/SimpleRedisSerializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-2015 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. @@ -29,6 +29,7 @@ /** * @author Jennifer Hickey + * @author Thomas Darimont */ public class SimpleRedisSerializerTests { @@ -157,4 +158,27 @@ public void testJsonSerializer() throws Exception { assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); } + @Test + public void testJackson2JsonSerializer() throws Exception { + + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Person.class); + String value = UUID.randomUUID().toString(); + Person p1 = new Person(value, value, 1, new Address(value, 2)); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + } + + /** + * @see DATAREDIS-390 + */ + @Test + public void testGenericJackson2JsonSerializer() throws Exception { + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + String value = UUID.randomUUID().toString(); + Person p1 = new Person(value, value, 1, new Address(value, 2)); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + assertEquals(p1, serializer.deserialize(serializer.serialize(p1))); + } + }