Register 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 #2677
This commit is contained in:
John Blum
2023-08-16 17:35:00 -07:00
committed by Mark Paluch
parent 2b9b8c2c9b
commit d89641e3a0
6 changed files with 178 additions and 18 deletions

View File

@@ -20,7 +20,9 @@ import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import org.springframework.core.convert.converter.Converter;
@@ -46,6 +48,24 @@ final class BinaryConverters {
private BinaryConverters() {}
static Collection<?> getConvertersToRegister() {
return Set.of(
new BinaryConverters.StringToBytesConverter(),
new BinaryConverters.BytesToStringConverter(),
new BinaryConverters.NumberToBytesConverter(),
new BinaryConverters.BytesToNumberConverterFactory(),
new BinaryConverters.EnumToBytesConverter(),
new BinaryConverters.BytesToEnumConverterFactory(),
new BinaryConverters.BooleanToBytesConverter(),
new BinaryConverters.BytesToBooleanConverter(),
new BinaryConverters.DateToBytesConverter(),
new BinaryConverters.BytesToDateConverter(),
new BinaryConverters.UuidToBytesConverter(),
new BinaryConverters.BytesToUuidConverter()
);
}
/**
* @author Christoph Strobl
* @since 1.7

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core.convert;
import java.time.Duration;
@@ -21,6 +20,8 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -47,9 +48,11 @@ public abstract class Jsr310Converters {
Jsr310Converters.class.getClassLoader());
/**
* Returns the converters to be registered. Will only return converters in case we're running on Java 8.
* Returns the {@link Converter Converters} to be registered.
* <p>
* Will only return {@link Converter Converters} in case we're running on Java 8.
*
* @return
* @return the {@link Converter Converters} to be registered.
*/
public static Collection<Converter<?, ?>> getConvertersToRegister() {
@@ -58,6 +61,7 @@ public abstract class Jsr310Converters {
}
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new LocalDateTimeToBytesConverter());
converters.add(new BytesToLocalDateTimeConverter());
converters.add(new LocalDateToBytesConverter());
@@ -74,6 +78,10 @@ public abstract class Jsr310Converters {
converters.add(new BytesToPeriodConverter());
converters.add(new DurationToBytesConverter());
converters.add(new BytesToDurationConverter());
converters.add(new OffsetDateTimeToBytesConverter());
converters.add(new BytesToOffsetDateTimeConverter());
converters.add(new OffsetTimeToBytesConverter());
converters.add(new BytesToOffsetTimeConverter());
return converters;
}
@@ -296,4 +304,51 @@ public abstract class Jsr310Converters {
}
}
/**
* @author John Blum
* @see java.time.OffsetDateTime
*/
static class OffsetDateTimeToBytesConverter extends StringBasedConverter implements Converter<OffsetDateTime, byte[]> {
@Override
public byte[] convert(OffsetDateTime source) {
return fromString(source.toString());
}
}
/**
* @author John Blum
* @see java.time.OffsetDateTime
*/
static class BytesToOffsetDateTimeConverter extends StringBasedConverter implements Converter<byte[], OffsetDateTime> {
@Override
public OffsetDateTime convert(byte[] source) {
return OffsetDateTime.parse(toString(source));
}
}
/**
* @author John Blum
* @see java.time.OffsetTime
*/
static class OffsetTimeToBytesConverter extends StringBasedConverter implements Converter<OffsetTime, byte[]> {
@Override
public byte[] convert(OffsetTime source) {
return fromString(source.toString());
}
}
/**
* @author John Blum
* @see java.time.OffsetTime
*/
static class BytesToOffsetTimeConverter extends StringBasedConverter implements Converter<byte[], OffsetTime> {
@Override
public OffsetTime convert(byte[] source) {
return OffsetTime.parse(toString(source));
}
}
}

View File

@@ -39,18 +39,7 @@ public class RedisCustomConversions extends org.springframework.data.convert.Cus
List<Object> converters = new ArrayList<>();
converters.add(new BinaryConverters.StringToBytesConverter());
converters.add(new BinaryConverters.BytesToStringConverter());
converters.add(new BinaryConverters.NumberToBytesConverter());
converters.add(new BinaryConverters.BytesToNumberConverterFactory());
converters.add(new BinaryConverters.EnumToBytesConverter());
converters.add(new BinaryConverters.BytesToEnumConverterFactory());
converters.add(new BinaryConverters.BooleanToBytesConverter());
converters.add(new BinaryConverters.BytesToBooleanConverter());
converters.add(new BinaryConverters.DateToBytesConverter());
converters.add(new BinaryConverters.BytesToDateConverter());
converters.add(new BinaryConverters.UuidToBytesConverter());
converters.add(new BinaryConverters.BytesToUuidConverter());
converters.addAll(BinaryConverters.getConvertersToRegister());
converters.addAll(Jsr310Converters.getConvertersToRegister());
STORE_CONVERTERS = Collections.unmodifiableList(converters);

View File

@@ -50,7 +50,7 @@ class RedisRepositoryClusterIntegrationTests extends RedisRepositoryIntegrationT
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
keyspaceConfiguration = MyKeyspaceConfiguration.class,
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class }) })
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class, UserRepository.class }) })
static class Config {
@Bean
@@ -62,6 +62,7 @@ class RedisRepositoryClusterIntegrationTests extends RedisRepositoryIntegrationT
connectionFactory.afterPropertiesSet();
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;

View File

@@ -21,6 +21,8 @@ import lombok.Data;
import lombok.Value;
import lombok.With;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -52,6 +54,9 @@ import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Base for testing Redis repository support in different configurations.
@@ -64,6 +69,7 @@ public abstract class RedisRepositoryIntegrationTestBase {
@Autowired PersonRepository repo;
@Autowired CityRepository cityRepo;
@Autowired ImmutableObjectRepository immutableObjectRepo;
@Autowired UserRepository userRepository;
@Autowired KeyValueTemplate kvTemplate;
@BeforeEach
@@ -497,6 +503,24 @@ public abstract class RedisRepositoryIntegrationTestBase {
assertThat(loaded.nested).isEqualTo(nested);
}
@Test // GH-2677
void shouldProperlyHandleEntityWithOffsetJavaTimeTypes() {
User jonDoe = User.as("Jon Doe")
.expires(OffsetTime.now().plusMinutes(5))
.lastAccess(OffsetDateTime.now());
this.userRepository.save(jonDoe);
User loadedJonDoe = this.userRepository.findById(jonDoe.getName()).orElse(null);
assertThat(loadedJonDoe).isNotNull();
assertThat(loadedJonDoe).isNotSameAs(jonDoe);
assertThat(loadedJonDoe.getName()).isEqualTo(jonDoe.getName());
assertThat(loadedJonDoe.getLastAccessed()).isEqualTo(jonDoe.getLastAccessed());
assertThat(loadedJonDoe.getExpiration()).isEqualTo(jonDoe.getExpiration());
}
public static interface PersonRepository
extends PagingAndSortingRepository<Person, String>, CrudRepository<Person, String>,
QueryByExampleExecutor<Person> {
@@ -544,6 +568,8 @@ public abstract class RedisRepositoryIntegrationTestBase {
public interface ImmutableObjectRepository extends CrudRepository<Immutable, String> {}
public interface UserRepository extends CrudRepository<User, String> { }
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
@@ -608,4 +634,72 @@ public abstract class RedisRepositoryIntegrationTestBase {
Immutable nested;
}
@RedisHash("Users")
static class User {
static User as(@NonNull String name) {
Assert.hasText(name, () -> String.format("Name [%s] of User is required", name));
return new User(name);
}
private OffsetDateTime lastAccessed;
private OffsetTime expiration;
@Id
private final String name;
private User(@NonNull String name) {
this.name = name;
}
@Nullable
public OffsetTime getExpiration() {
return this.expiration;
}
@Nullable
public OffsetDateTime getLastAccessed() {
return this.lastAccessed;
}
public String getName() {
return this.name;
}
public User lastAccess(@Nullable OffsetDateTime dateTime) {
this.lastAccessed = dateTime;
return this;
}
public User expires(@Nullable OffsetTime time) {
this.expiration = time;
return this;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof User that)) {
return false;
}
return this.getName().equals(that.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName());
}
@Override
public String toString() {
return getName();
}
}
}

View File

@@ -56,8 +56,7 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
keyspaceConfiguration = MyKeyspaceConfiguration.class,
includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class }) })
classes = { PersonRepository.class, CityRepository.class, ImmutableObjectRepository.class, UserRepository.class }) })
static class Config {
@Bean
@@ -67,6 +66,7 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
connectionFactory.afterPropertiesSet();
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setDefaultSerializer(StringRedisSerializer.UTF_8);
template.setConnectionFactory(connectionFactory);
@@ -104,6 +104,7 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
public void shouldConsiderCustomTypeMapper() {
Person rand = new Person();
rand.id = "rand";
rand.firstname = "rand";
rand.lastname = "al'thor";