Skip to content

Commit 9a57424

Browse files
committed
Registers Converters for Offset java.time types in JSR310Converters.
We now appropriately handle OffsetDateTime and OffsetTime the same as all other java.time types, supported as simple types on Spring application (persistent) entity classes. Closes spring-projects#2677
1 parent 0358ef1 commit 9a57424

File tree

6 files changed

+178
-19
lines changed

6 files changed

+178
-19
lines changed

src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.text.DateFormat;
2121
import java.text.ParseException;
2222
import java.util.Arrays;
23+
import java.util.Collection;
2324
import java.util.Date;
25+
import java.util.Set;
2426
import java.util.UUID;
2527

2628
import org.springframework.core.convert.converter.Converter;
@@ -46,6 +48,24 @@ final class BinaryConverters {
4648

4749
private BinaryConverters() {}
4850

51+
static Collection<?> getConvertersToRegister() {
52+
53+
return Set.of(
54+
new BinaryConverters.StringToBytesConverter(),
55+
new BinaryConverters.BytesToStringConverter(),
56+
new BinaryConverters.NumberToBytesConverter(),
57+
new BinaryConverters.BytesToNumberConverterFactory(),
58+
new BinaryConverters.EnumToBytesConverter(),
59+
new BinaryConverters.BytesToEnumConverterFactory(),
60+
new BinaryConverters.BooleanToBytesConverter(),
61+
new BinaryConverters.BytesToBooleanConverter(),
62+
new BinaryConverters.DateToBytesConverter(),
63+
new BinaryConverters.BytesToDateConverter(),
64+
new BinaryConverters.UuidToBytesConverter(),
65+
new BinaryConverters.BytesToUuidConverter()
66+
);
67+
}
68+
4969
/**
5070
* @author Christoph Strobl
5171
* @since 1.7

src/main/java/org/springframework/data/redis/core/convert/Jsr310Converters.java

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package org.springframework.data.redis.core.convert;
1817

1918
import java.time.Duration;
2019
import java.time.Instant;
2120
import java.time.LocalDate;
2221
import java.time.LocalDateTime;
2322
import java.time.LocalTime;
23+
import java.time.OffsetDateTime;
24+
import java.time.OffsetTime;
2425
import java.time.Period;
2526
import java.time.ZoneId;
2627
import java.time.ZonedDateTime;
@@ -47,9 +48,11 @@ public abstract class Jsr310Converters {
4748
Jsr310Converters.class.getClassLoader());
4849

4950
/**
50-
* Returns the converters to be registered. Will only return converters in case we're running on Java 8.
51+
* Returns the {@link Converter Converters} to be registered.
52+
* <p>
53+
* Will only return {@link Converter Converters} in case we're running on Java 8.
5154
*
52-
* @return
55+
* @return the {@link Converter Converters} to be registered.
5356
*/
5457
public static Collection<Converter<?, ?>> getConvertersToRegister() {
5558

@@ -58,6 +61,7 @@ public abstract class Jsr310Converters {
5861
}
5962

6063
List<Converter<?, ?>> converters = new ArrayList<>();
64+
6165
converters.add(new LocalDateTimeToBytesConverter());
6266
converters.add(new BytesToLocalDateTimeConverter());
6367
converters.add(new LocalDateToBytesConverter());
@@ -74,6 +78,10 @@ public abstract class Jsr310Converters {
7478
converters.add(new BytesToPeriodConverter());
7579
converters.add(new DurationToBytesConverter());
7680
converters.add(new BytesToDurationConverter());
81+
converters.add(new OffsetDateTimeToBytesConverter());
82+
converters.add(new BytesToOffsetDateTimeConverter());
83+
converters.add(new OffsetTimeToBytesConverter());
84+
converters.add(new BytesToOffsetTimeConverter());
7785

7886
return converters;
7987
}
@@ -296,4 +304,51 @@ public Duration convert(byte[] source) {
296304
}
297305
}
298306

307+
/**
308+
* @author John Blum
309+
* @see java.time.OffsetDateTime
310+
*/
311+
static class OffsetDateTimeToBytesConverter extends StringBasedConverter implements Converter<OffsetDateTime, byte[]> {
312+
313+
@Override
314+
public byte[] convert(OffsetDateTime source) {
315+
return fromString(source.toString());
316+
}
317+
}
318+
319+
/**
320+
* @author John Blum
321+
* @see java.time.OffsetDateTime
322+
*/
323+
static class BytesToOffsetDateTimeConverter extends StringBasedConverter implements Converter<byte[], OffsetDateTime> {
324+
325+
@Override
326+
public OffsetDateTime convert(byte[] source) {
327+
return OffsetDateTime.parse(toString(source));
328+
}
329+
}
330+
331+
/**
332+
* @author John Blum
333+
* @see java.time.OffsetTime
334+
*/
335+
static class OffsetTimeToBytesConverter extends StringBasedConverter implements Converter<OffsetTime, byte[]> {
336+
337+
@Override
338+
public byte[] convert(OffsetTime source) {
339+
return fromString(source.toString());
340+
}
341+
}
342+
343+
/**
344+
* @author John Blum
345+
* @see java.time.OffsetTime
346+
*/
347+
static class BytesToOffsetTimeConverter extends StringBasedConverter implements Converter<byte[], OffsetTime> {
348+
349+
@Override
350+
public OffsetTime convert(byte[] source) {
351+
return OffsetTime.parse(toString(source));
352+
}
353+
}
299354
}

src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,7 @@ public class RedisCustomConversions extends org.springframework.data.convert.Cus
3939

4040
List<Object> converters = new ArrayList<>();
4141

42-
converters.add(new BinaryConverters.StringToBytesConverter());
43-
converters.add(new BinaryConverters.BytesToStringConverter());
44-
converters.add(new BinaryConverters.NumberToBytesConverter());
45-
converters.add(new BinaryConverters.BytesToNumberConverterFactory());
46-
converters.add(new BinaryConverters.EnumToBytesConverter());
47-
converters.add(new BinaryConverters.BytesToEnumConverterFactory());
48-
converters.add(new BinaryConverters.BooleanToBytesConverter());
49-
converters.add(new BinaryConverters.BytesToBooleanConverter());
50-
converters.add(new BinaryConverters.DateToBytesConverter());
51-
converters.add(new BinaryConverters.BytesToDateConverter());
52-
converters.add(new BinaryConverters.UuidToBytesConverter());
53-
converters.add(new BinaryConverters.BytesToUuidConverter());
42+
converters.addAll(BinaryConverters.getConvertersToRegister());
5443
converters.addAll(Jsr310Converters.getConvertersToRegister());
5544

5645
STORE_CONVERTERS = Collections.unmodifiableList(converters);

src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.springframework.data.redis.core.RedisTemplate;
3333
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
3434
import org.springframework.data.redis.test.condition.EnabledOnRedisClusterAvailable;
35-
import org.springframework.lang.NonNullApi;
3635
import org.springframework.test.context.ContextConfiguration;
3736
import org.springframework.test.context.junit.jupiter.SpringExtension;
3837

@@ -52,13 +51,14 @@ class RedisRepositoryClusterIntegrationTests extends RedisRepositoryIntegrationT
5251
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
5352
keyspaceConfiguration = MyKeyspaceConfiguration.class,
5453
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
55-
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class }) })
54+
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class, UserRepository.class }) })
5655
static class Config {
5756

5857
@Bean
5958
RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
6059

6160
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
61+
6262
template.setConnectionFactory(connectionFactory);
6363

6464
return template;

src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919

20+
import java.time.OffsetDateTime;
21+
import java.time.OffsetTime;
2022
import java.util.Arrays;
2123
import java.util.Collections;
2224
import java.util.List;
@@ -49,6 +51,9 @@
4951
import org.springframework.data.repository.CrudRepository;
5052
import org.springframework.data.repository.PagingAndSortingRepository;
5153
import org.springframework.data.repository.query.QueryByExampleExecutor;
54+
import org.springframework.lang.NonNull;
55+
import org.springframework.lang.Nullable;
56+
import org.springframework.util.Assert;
5257

5358
/**
5459
* Base for testing Redis repository support in different configurations.
@@ -62,6 +67,7 @@ public abstract class RedisRepositoryIntegrationTestBase {
6267
@Autowired PersonRepository repo;
6368
@Autowired CityRepository cityRepo;
6469
@Autowired ImmutableObjectRepository immutableObjectRepo;
70+
@Autowired UserRepository userRepository;
6571
@Autowired KeyValueTemplate kvTemplate;
6672

6773
@BeforeEach
@@ -495,6 +501,24 @@ void shouldProperlyReadNestedImmutableObject() {
495501
assertThat(loaded.nested).isEqualTo(nested);
496502
}
497503

504+
@Test // GH-2677
505+
void shouldProperlyHandleEntityWithOffsetJavaTimeTypes() {
506+
507+
User jonDoe = User.as("Jon Doe")
508+
.expires(OffsetTime.now().plusMinutes(5))
509+
.lastAccess(OffsetDateTime.now());
510+
511+
this.userRepository.save(jonDoe);
512+
513+
User loadedJonDoe = this.userRepository.findById(jonDoe.getName()).orElse(null);
514+
515+
assertThat(loadedJonDoe).isNotNull();
516+
assertThat(loadedJonDoe).isNotSameAs(jonDoe);
517+
assertThat(loadedJonDoe.getName()).isEqualTo(jonDoe.getName());
518+
assertThat(loadedJonDoe.getLastAccessed()).isEqualTo(jonDoe.getLastAccessed());
519+
assertThat(loadedJonDoe.getExpiration()).isEqualTo(jonDoe.getExpiration());
520+
}
521+
498522
public interface PersonRepository
499523
extends PagingAndSortingRepository<Person, String>, CrudRepository<Person, String>,
500524
QueryByExampleExecutor<Person> {
@@ -542,6 +566,8 @@ public interface CityRepository extends CrudRepository<City, String> {
542566

543567
public interface ImmutableObjectRepository extends CrudRepository<Immutable, String> {}
544568

569+
public interface UserRepository extends CrudRepository<User, String> { }
570+
545571
/**
546572
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
547573
*
@@ -784,4 +810,72 @@ public Immutable withNested(Immutable nested) {
784810
return Objects.equals(getNested(), nested) ? this : new Immutable(this.id, this.name, nested);
785811
}
786812
}
813+
814+
@RedisHash("Users")
815+
static class User {
816+
817+
static User as(@NonNull String name) {
818+
Assert.hasText(name, () -> String.format("Name [%s] of User is required", name));
819+
return new User(name);
820+
}
821+
822+
private OffsetDateTime lastAccessed;
823+
824+
private OffsetTime expiration;
825+
826+
@Id
827+
private final String name;
828+
829+
private User(@NonNull String name) {
830+
this.name = name;
831+
}
832+
833+
@Nullable
834+
public OffsetTime getExpiration() {
835+
return this.expiration;
836+
}
837+
838+
@Nullable
839+
public OffsetDateTime getLastAccessed() {
840+
return this.lastAccessed;
841+
}
842+
843+
public String getName() {
844+
return this.name;
845+
}
846+
847+
public User lastAccess(@Nullable OffsetDateTime dateTime) {
848+
this.lastAccessed = dateTime;
849+
return this;
850+
}
851+
852+
public User expires(@Nullable OffsetTime time) {
853+
this.expiration = time;
854+
return this;
855+
}
856+
857+
@Override
858+
public boolean equals(Object obj) {
859+
860+
if (this == obj) {
861+
return true;
862+
}
863+
864+
if (!(obj instanceof User that)) {
865+
return false;
866+
}
867+
868+
return this.getName().equals(that.getName());
869+
}
870+
871+
@Override
872+
public int hashCode() {
873+
return Objects.hash(getName());
874+
}
875+
876+
@Override
877+
public String toString() {
878+
return getName();
879+
}
880+
}
787881
}

src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
5757
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
5858
keyspaceConfiguration = MyKeyspaceConfiguration.class,
5959
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
60-
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class }) })
61-
60+
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class, UserRepository.class }) })
6261
static class Config {
6362

6463
@Bean
@@ -70,6 +69,7 @@ RedisConnectionFactory connectionFactory() {
7069
RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
7170

7271
RedisTemplate<String, String> template = new RedisTemplate<>();
72+
7373
template.setDefaultSerializer(StringRedisSerializer.UTF_8);
7474
template.setConnectionFactory(connectionFactory);
7575

@@ -107,6 +107,7 @@ private RedisTypeMapper customTypeMapper() {
107107
public void shouldConsiderCustomTypeMapper() {
108108

109109
Person rand = new Person();
110+
110111
rand.id = "rand";
111112
rand.firstname = "rand";
112113
rand.lastname = "al'thor";

0 commit comments

Comments
 (0)