Skip to content

Commit 828cad1

Browse files
DATAGRAPH-1388 - Add "legacy" converters for DateLong and DateString.
Those new legacy converters use the infrastructure introduced in the previous commit and also provides the appropriate integration tests.
1 parent b2ee6ca commit 828cad1

File tree

5 files changed

+235
-6
lines changed

5 files changed

+235
-6
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2011-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.support;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Inherited;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
import java.util.Date;
24+
25+
import org.apiguardian.api.API;
26+
import org.neo4j.driver.Value;
27+
import org.neo4j.driver.Values;
28+
import org.springframework.data.neo4j.core.convert.ConvertWith;
29+
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
30+
31+
/**
32+
* Indicates SDN to store dates as long in the database.
33+
* Applicable to `java.util.Date` and `java.time.Instant`
34+
*
35+
* @author Michael J. Simons
36+
* @soundtrack Linkin Park - One More Light Live
37+
* @since 6.0
38+
*/
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Target({ ElementType.FIELD })
41+
@Inherited
42+
@ConvertWith(converter = DateLongConverter.class)
43+
@API(status = API.Status.STABLE, since = "6.0")
44+
public @interface DateLong {
45+
}
46+
47+
final class DateLongConverter implements Neo4jPersistentPropertyConverter<Date> {
48+
49+
@Override
50+
public Value write(Date source) {
51+
return Values.value(source.getTime());
52+
}
53+
54+
@Override
55+
public Date read(Value source) {
56+
return new Date(source.asLong());
57+
}
58+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2011-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.neo4j.core.support;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Inherited;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
import java.text.ParseException;
24+
import java.text.SimpleDateFormat;
25+
import java.util.Date;
26+
import java.util.TimeZone;
27+
28+
import org.apiguardian.api.API;
29+
import org.neo4j.driver.Value;
30+
import org.neo4j.driver.Values;
31+
import org.springframework.core.annotation.AliasFor;
32+
import org.springframework.data.neo4j.core.convert.ConvertWith;
33+
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverterFactory;
34+
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
35+
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
36+
37+
/**
38+
* Indicates SDN 6 to store dates as {@link String} in the database. Applicable to {@link Date} and
39+
* {@link java.time.Instant}.
40+
*
41+
* @author Michael J. Simons
42+
* @soundtrack Metallica - S&M2
43+
* @since 6.0
44+
*/
45+
@Retention(RetentionPolicy.RUNTIME)
46+
@Target({ ElementType.FIELD })
47+
@Inherited
48+
@ConvertWith(converterFactory = DateStringConverterFactory.class)
49+
@API(status = API.Status.STABLE, since = "6.0")
50+
public @interface DateString {
51+
52+
String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
53+
54+
String DEFAULT_ZONE_ID = "UTC";
55+
56+
@AliasFor("format")
57+
String value() default ISO_8601;
58+
59+
@AliasFor("value")
60+
String format() default ISO_8601;
61+
62+
/**
63+
* Some temporals like {@link java.time.Instant}, representing an instantaneous point in time cannot be formatted
64+
* with a given {@link java.time.ZoneId}. In case you want to format an instant or similar with a default pattern,
65+
* we assume a zone with the given id and default to {@literal UTC} which is the same assumption that the predefined
66+
* patterns in {@link java.time.format.DateTimeFormatter} take.
67+
*
68+
* @return The zone id to use when applying a custom pattern to an instant temporal.
69+
*/
70+
String zoneId() default DEFAULT_ZONE_ID;
71+
}
72+
73+
final class DateStringConverterFactory implements Neo4jPersistentPropertyConverterFactory {
74+
75+
@Override
76+
public Neo4jPersistentPropertyConverter getPropertyConverterFor(Neo4jPersistentProperty persistentProperty) {
77+
78+
if (persistentProperty.getActualType() == Date.class) {
79+
DateString config = persistentProperty.getRequiredAnnotation(DateString.class);
80+
return new DateStringConverter(config.value());
81+
} else {
82+
throw new UnsupportedOperationException(
83+
"Other types than java.util.Date are not yet supported. Please file a ticket.");
84+
}
85+
}
86+
}
87+
88+
final class DateStringConverter implements Neo4jPersistentPropertyConverter<Date> {
89+
90+
private final String format;
91+
92+
DateStringConverter(String format) {
93+
this.format = format;
94+
}
95+
96+
private SimpleDateFormat getFormat() {
97+
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
98+
simpleDateFormat.setTimeZone(TimeZone.getTimeZone(DateString.DEFAULT_ZONE_ID));
99+
return simpleDateFormat;
100+
}
101+
102+
@Override
103+
public Value write(Date source) {
104+
return Values.value(getFormat().format(source));
105+
}
106+
107+
@Override
108+
public Date read(Value source) {
109+
try {
110+
return getFormat().parse(source.asString());
111+
} catch (ParseException e) {
112+
throw new RuntimeException(e);
113+
}
114+
}
115+
116+
}

src/test/java/org/springframework/data/neo4j/integration/imperative/TypeConversionIT.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2020

21+
import java.lang.reflect.Field;
22+
import java.text.SimpleDateFormat;
23+
import java.time.ZoneId;
24+
import java.time.ZonedDateTime;
25+
import java.time.temporal.ChronoUnit;
2126
import java.util.Arrays;
2227
import java.util.Collection;
2328
import java.util.Collections;
29+
import java.util.Date;
2430
import java.util.HashMap;
31+
import java.util.List;
2532
import java.util.Map;
33+
import java.util.Optional;
2634
import java.util.UUID;
35+
import java.util.function.Function;
2736
import java.util.stream.Stream;
2837

2938
import org.assertj.core.api.Assertions;
@@ -32,6 +41,7 @@
3241
import org.junit.jupiter.api.DynamicTest;
3342
import org.junit.jupiter.api.Test;
3443
import org.junit.jupiter.api.TestFactory;
44+
import org.junit.platform.commons.util.AnnotationUtils;
3545
import org.neo4j.driver.Driver;
3646
import org.neo4j.driver.Session;
3747
import org.neo4j.driver.Value;
@@ -44,6 +54,7 @@
4454
import org.springframework.data.mapping.MappingException;
4555
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
4656
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
57+
import org.springframework.data.neo4j.core.convert.ConvertWith;
4758
import org.springframework.data.neo4j.integration.shared.Neo4jConversionsITBase;
4859
import org.springframework.data.neo4j.integration.shared.ThingWithAllAdditionalTypes;
4960
import org.springframework.data.neo4j.integration.shared.ThingWithAllCypherTypes;
@@ -56,6 +67,7 @@
5667
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
5768
import org.springframework.test.util.ReflectionTestUtils;
5869
import org.springframework.transaction.annotation.EnableTransactionManagement;
70+
import org.springframework.util.ReflectionUtils;
5971

6072
/**
6173
* @author Michael J. Simons
@@ -157,14 +169,24 @@ void assertWrite(Object thing, String fieldName, ConversionService conversionSer
157169
long id = (long) ReflectionTestUtils.getField(thing, "id");
158170
Object domainValue = ReflectionTestUtils.getField(thing, fieldName);
159171

172+
Field field = ReflectionUtils.findField(thing.getClass(), fieldName);
173+
Optional<ConvertWith> annotation = AnnotationUtils.findAnnotation(field, ConvertWith.class);
174+
Function<Object, Value> conversion;
175+
if (fieldName.equals("dateAsLong")) {
176+
conversion = o -> Values.value(((Date) o).getTime());
177+
} else if (fieldName.equals("dateAsString")) {
178+
conversion = o -> Values.value(new SimpleDateFormat("yyyy-MM-dd").format(o));
179+
} else {
180+
conversion = o -> conversionService.convert(o, Value.class);
181+
}
160182
Value driverValue;
161183
if (domainValue != null && Collection.class.isAssignableFrom(domainValue.getClass())) {
162184
Collection<?> sourceCollection = (Collection<?>) domainValue;
163185
Object[] targetCollection = (sourceCollection).stream()
164-
.map(element -> conversionService.convert(element, Value.class)).toArray();
186+
.map(element -> conversion.apply(element)).toArray();
165187
driverValue = Values.value(targetCollection);
166188
} else {
167-
driverValue = conversionService.convert(domainValue, Value.class);
189+
driverValue = conversion.apply(domainValue);
168190
}
169191

170192
try (Session session = neo4jConnectionSupport.getDriver().session()) {
@@ -202,6 +224,13 @@ void relatedIdsShouldBeConverted(@Autowired ConvertedIDsRepository repository) {
202224
Assertions.assertThat(repository.findById(savedThing.getAnotherThing().getId())).isPresent();
203225
}
204226

227+
@Test
228+
void parametersTargetingConvertedAttributesMustBeConverted(@Autowired CustomTypesRepository repository) {
229+
230+
assertThat(repository.findAllByDateAsString(Date.from(ZonedDateTime.of(2013, 5, 6,
231+
12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant().truncatedTo(ChronoUnit.DAYS)))).hasSizeGreaterThan(0);
232+
}
233+
205234
public interface ConvertedIDsRepository extends Neo4jRepository<ThingWithUUIDID, UUID> {}
206235

207236
public interface CypherTypesRepository extends Neo4jRepository<ThingWithAllCypherTypes, Long> {}
@@ -212,7 +241,10 @@ public interface SpatialTypesRepository extends Neo4jRepository<ThingWithAllSpat
212241

213242
public interface NonExistingPrimitivesRepository extends Neo4jRepository<ThingWithNonExistingPrimitives, Long> {}
214243

215-
public interface CustomTypesRepository extends Neo4jRepository<ThingWithCustomTypes, Long> {}
244+
public interface CustomTypesRepository extends Neo4jRepository<ThingWithCustomTypes, Long> {
245+
246+
List<ThingWithCustomTypes> findAllByDateAsString(Date theDate);
247+
}
216248

217249
@Configuration
218250
@EnableNeo4jRepositories(considerNestedRepositories = true)

src/test/java/org/springframework/data/neo4j/integration/shared/Neo4jConversionsITBase.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.time.ZoneId;
3333
import java.time.ZoneOffset;
3434
import java.time.ZonedDateTime;
35+
import java.time.temporal.ChronoUnit;
3536
import java.util.ArrayList;
3637
import java.util.Arrays;
3738
import java.util.Collections;
@@ -172,6 +173,10 @@ GeographicPoint3d asGeo3d(double height) {
172173
static {
173174
Map<String, Object> hlp = new HashMap<>();
174175
hlp.put("customType", ThingWithCustomTypes.CustomType.of("ABCD"));
176+
hlp.put("dateAsLong", Date.from(ZonedDateTime.of(2020, 9, 21,
177+
12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant()));
178+
hlp.put("dateAsString", Date.from(ZonedDateTime.of(2013, 5, 6,
179+
12, 0, 0, 0, ZoneId.of("Europe/Berlin")).toInstant().truncatedTo(ChronoUnit.DAYS)));
175180
CUSTOM_TYPES = Collections.unmodifiableMap(hlp);
176181
}
177182

@@ -238,8 +243,10 @@ static void prepareData() {
238243

239244
parameters = new HashMap<>();
240245
parameters.put("customType", "ABCD");
246+
parameters.put("dateAsLong", 1600682400000L);
247+
parameters.put("dateAsString", "2013-05-06");
241248
ID_OF_CUSTOM_TYPE_NODE = w
242-
.run("CREATE (n:CustomTypes) SET " + " n.customType = $customType" + " RETURN id(n) AS id", parameters)
249+
.run("CREATE (n:CustomTypes) SET n.customType = $customType, n.dateAsLong = $dateAsLong, n.dateAsString = $dateAsString RETURN id(n) AS id", parameters)
243250
.single().get("id").asLong();
244251
w.commit();
245252
return null;

src/test/java/org/springframework/data/neo4j/integration/shared/ThingWithCustomTypes.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.neo4j.integration.shared;
1717

18+
import java.util.Date;
1819
import java.util.HashSet;
1920
import java.util.Objects;
2021
import java.util.Set;
@@ -27,9 +28,12 @@
2728
import org.springframework.data.neo4j.core.schema.GeneratedValue;
2829
import org.springframework.data.neo4j.core.schema.Id;
2930
import org.springframework.data.neo4j.core.schema.Node;
31+
import org.springframework.data.neo4j.core.support.DateLong;
32+
import org.springframework.data.neo4j.core.support.DateString;
3033

3134
/**
3235
* @author Gerrit Meier
36+
* @author Michael J. Simons
3337
*/
3438
@Node("CustomTypes")
3539
public class ThingWithCustomTypes {
@@ -38,19 +42,31 @@ public class ThingWithCustomTypes {
3842

3943
private CustomType customType;
4044

41-
public ThingWithCustomTypes(Long id, CustomType customType) {
45+
@DateLong
46+
private Date dateAsLong;
47+
48+
@DateString("yyyy-MM-dd")
49+
private Date dateAsString;
50+
51+
public ThingWithCustomTypes(Long id, CustomType customType, Date dateAsLong, Date dateAsString) {
4252
this.id = id;
4353
this.customType = customType;
54+
this.dateAsLong = dateAsLong;
55+
this.dateAsString = dateAsString;
4456
}
4557

4658
public ThingWithCustomTypes withId(Long newId) {
47-
return new ThingWithCustomTypes(newId, this.customType);
59+
return new ThingWithCustomTypes(newId, this.customType, this.dateAsLong, this.dateAsString);
4860
}
4961

5062
public CustomType getCustomType() {
5163
return customType;
5264
}
5365

66+
public void setDateAsLong(Date dateAsLong) {
67+
this.dateAsLong = dateAsLong;
68+
}
69+
5470
/**
5571
* Custom type to convert
5672
*/

0 commit comments

Comments
 (0)