From 935f9f503b29455a1671e0cfcf159e0bc1913378 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 6 Mar 2020 16:45:25 +0100 Subject: [PATCH] GH-773 - Add zoneId to [At]DateString. This attribute will be used for converting attributes to Strings that cannot be converted without the knowledge of a valid zone (for example `java.time.Instant`). The zoneId defaults to `UTC` which is the same as the default patterns use. The default is required to not break existing users. For the time being, the zoneId is only used in the `org.neo4j.ogm.typeconversion.InstantStringConverter`. This closes #771. --- .../annotation/typeconversion/DateString.java | 11 ++++ .../neo4j/ogm/metadata/ObjectAnnotations.java | 2 +- .../InstantStringConverter.java | 5 +- .../date/InstantStringConverter.java | 42 ------------ .../ogm/domain/convertible/date/Memo.java | 22 +++++++ .../ogm/metadata/ClassPathScannerTest.java | 2 +- .../ConvertibleIntegrationTest.java | 66 ++++++++++--------- 7 files changed, 72 insertions(+), 78 deletions(-) delete mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/InstantStringConverter.java diff --git a/core/src/main/java/org/neo4j/ogm/annotation/typeconversion/DateString.java b/core/src/main/java/org/neo4j/ogm/annotation/typeconversion/DateString.java index b480ada721..0ada81b0b8 100644 --- a/core/src/main/java/org/neo4j/ogm/annotation/typeconversion/DateString.java +++ b/core/src/main/java/org/neo4j/ogm/annotation/typeconversion/DateString.java @@ -40,8 +40,19 @@ String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + String DEFAULT_ZONE_ID = "UTC"; + String value() default ISO_8601; + /** + * Some temporals like {@link java.time.Instant}, representing an instantaneous point in time cannot be formatted + * with a given {@link java.time.ZoneId}. In case you want to format an instant or similar with a default pattern, + * we assume a zone with the given id and default to {@literal UTC} which is the same assumption that the predefined + * patterns in {@link java.time.format.DateTimeFormatter} take. + * @return The zone id to use when applying a custom pattern to an instant temporal. + */ + String zoneId() default DEFAULT_ZONE_ID; + /** * Toggle lenient conversion mode by setting this flag to true (defaults to false). * Has to be supported by the corresponding converter. diff --git a/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java b/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java index 7bb720f050..d9b2d9a261 100644 --- a/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java +++ b/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java @@ -108,7 +108,7 @@ Object getConverter(Class fieldType) { if (fieldType == Date.class) { return new DateStringConverter(format, isLenientConversion(dateStringConverterInfo)); } else if (fieldType == Instant.class) { - return new InstantStringConverter(format, isLenientConversion(dateStringConverterInfo)); + return new InstantStringConverter(format, dateStringConverterInfo.get("zoneId"), isLenientConversion(dateStringConverterInfo)); } else { throw new MappingException("Cannot use @DateString with attribute of type " + fieldType); } diff --git a/core/src/main/java/org/neo4j/ogm/typeconversion/InstantStringConverter.java b/core/src/main/java/org/neo4j/ogm/typeconversion/InstantStringConverter.java index 5f476404cf..e89686ca3e 100644 --- a/core/src/main/java/org/neo4j/ogm/typeconversion/InstantStringConverter.java +++ b/core/src/main/java/org/neo4j/ogm/typeconversion/InstantStringConverter.java @@ -19,6 +19,7 @@ package org.neo4j.ogm.typeconversion; import java.time.Instant; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import org.apache.commons.lang3.StringUtils; @@ -45,11 +46,11 @@ public InstantStringConverter() { this.lenient = false; } - public InstantStringConverter(String userDefinedFormat, boolean lenient) { + public InstantStringConverter(String userDefinedFormat, String zoneId, boolean lenient) { this.formatter = DateString.ISO_8601.equals(userDefinedFormat) ? DateTimeFormatter.ISO_INSTANT : - DateTimeFormatter.ofPattern(userDefinedFormat); + DateTimeFormatter.ofPattern(userDefinedFormat).withZone(ZoneId.of(zoneId)); this.lenient = lenient; } diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/InstantStringConverter.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/InstantStringConverter.java deleted file mode 100644 index 7a9b8c4000..0000000000 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/InstantStringConverter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2002-2020 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * 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.neo4j.ogm.domain.convertible.date; - -import java.time.Instant; - -import org.neo4j.ogm.typeconversion.AttributeConverter; -import org.neo4j.ogm.typeconversion.DateStringConverter; - -/** - * @author Vince Bickers - */ -public class InstantStringConverter implements AttributeConverter { - - private final DateStringConverter converter = new DateStringConverter("yyyyMMddhhmmss"); - - @Override - public String toGraphProperty(Instant value) { - return null; - } - - @Override - public Instant toEntityAttribute(String value) { - return null; - } -} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/Memo.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/Memo.java index 84450b7261..f672c41d46 100644 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/Memo.java +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/convertible/date/Memo.java @@ -58,6 +58,12 @@ public class Memo { @DateString private Instant actionedAsInstant; + @DateString(value = "yyyy-MM-dd HH:mm:ss") + private Instant actionedAsInstantWithCustomFormat1; + + @DateString(value = "yyyy-MM-dd HH:mm:ss", zoneId = "Europe/Berlin") + private Instant actionedAsInstantWithCustomFormat2; + // uses default ISO 8601 date format private Date[] escalations; @@ -154,4 +160,20 @@ public Set getImplementations() { public void setImplementations(Set implementations) { this.implementations = implementations; } + + public Instant getActionedAsInstantWithCustomFormat1() { + return actionedAsInstantWithCustomFormat1; + } + + public void setActionedAsInstantWithCustomFormat1(Instant actionedAsInstantWithCustomFormat1) { + this.actionedAsInstantWithCustomFormat1 = actionedAsInstantWithCustomFormat1; + } + + public Instant getActionedAsInstantWithCustomFormat2() { + return actionedAsInstantWithCustomFormat2; + } + + public void setActionedAsInstantWithCustomFormat2(Instant actionedAsInstantWithCustomFormat2) { + this.actionedAsInstantWithCustomFormat2 = actionedAsInstantWithCustomFormat2; + } } diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/metadata/ClassPathScannerTest.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/metadata/ClassPathScannerTest.java index 9bff4128d3..2dcd226bb1 100644 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/metadata/ClassPathScannerTest.java +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/metadata/ClassPathScannerTest.java @@ -49,7 +49,7 @@ public void directoryShouldBeScanned() { public void nestedDirectoryShouldBeScanned() { final DomainInfo domainInfo = DomainInfo.create("org.neo4j.ogm.domain.convertible"); - assertThat(domainInfo.getClassInfoMap()).hasSize(21); + assertThat(domainInfo.getClassInfoMap()).hasSize(20); Set classNames = domainInfo.getClassInfoMap().keySet(); assertThat(classNames.contains("org.neo4j.ogm.domain.convertible.bytes.Photo")).isTrue(); diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/convertible/ConvertibleIntegrationTest.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/convertible/ConvertibleIntegrationTest.java index 2e0bb9d7b8..35725b1c9e 100644 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/convertible/ConvertibleIntegrationTest.java +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/convertible/ConvertibleIntegrationTest.java @@ -23,13 +23,14 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; @@ -52,13 +53,14 @@ /** * @author Luanne Misquitta + * @author Michael J. Simons */ public class ConvertibleIntegrationTest extends TestContainersTestBase { private static Session session; @BeforeClass - public static void init() throws IOException { + public static void init() { session = new SessionFactory(getDriver(), "org.neo4j.ogm.domain.convertible").openSession(); } @@ -67,10 +69,7 @@ public void teardown() { session.purgeDatabase(); } - /** - * @see DATAGRAPH-550 - */ - @Test + @Test // DATAGRAPH-550 public void shouldSaveAndRetrieveEnums() { List completed = new ArrayList<>(); completed.add(Education.HIGHSCHOOL); @@ -121,7 +120,7 @@ public void shouldSaveAndRetrieveEnumsAsResult() { .equals(Education.PHD)).isTrue(); } - @Test // DATAGRAPH-550 GH-758 + @Test // DATAGRAPH-550 GH-758 GH-771 public void shouldSaveAndRetrieveDates() { SimpleDateFormat simpleDateISO8601format = new SimpleDateFormat(DateString.ISO_8601); simpleDateISO8601format.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -186,6 +185,32 @@ public void shouldSaveAndRetrieveDates() { assertThat(loadedCal.get(Calendar.YEAR)).isEqualTo(date100000.get(Calendar.YEAR)); } + @Test // GH-771 + public void instantsWithCustomFormatAndTZShouldWork() { + + Memo memo = new Memo(); + memo.setMemo("theMemo"); + Instant actionedInstant = ZonedDateTime.of(2020, 3, 6, 16, 6, 23, 0, ZoneId.of("Europe/Berlin")).toInstant(); + memo.setActionedAsInstant(actionedInstant); + memo.setActionedAsInstantWithCustomFormat1(actionedInstant); + memo.setActionedAsInstantWithCustomFormat2(actionedInstant); + session.save(memo); + + Memo loadedMemo = session.loadAll(Memo.class, new Filter("memo", ComparisonOperator.EQUALS, "theMemo")) + .iterator().next(); + + assertThat(loadedMemo.getActionedAsInstantWithCustomFormat1()).isEqualTo(actionedInstant); + assertThat(loadedMemo.getActionedAsInstantWithCustomFormat2()).isEqualTo(actionedInstant); + + Iterable> results = session.query( + "MATCH (m:Memo) WHERE id(m) = $id RETURN m.actionedAsInstantWithCustomFormat1 as a1, m.actionedAsInstantWithCustomFormat2 as a2", + Collections.singletonMap("id", loadedMemo.getId())).queryResults(); + assertThat(results).hasSize(1); + Map result = results.iterator().next(); + assertThat(result).containsEntry("a1", "2020-03-06 15:06:23"); + assertThat(result).containsEntry("a2", "2020-03-06 16:06:23"); + } + @Test public void shouldSaveAndRetrieveJava8Dates() { @@ -209,7 +234,7 @@ public void shouldSaveAndRetrieveJava8Dates() { } @Test - public void shouldSaveListOfLocalDate() throws Exception { + public void shouldSaveListOfLocalDate() { Java8DatesMemo memo = new Java8DatesMemo(); @@ -231,7 +256,7 @@ public void shouldSaveListOfLocalDate() throws Exception { } @Test - public void shouldSaveLocalDateTime() throws Exception { + public void shouldSaveLocalDateTime() { Java8DatesMemo memo = new Java8DatesMemo(); @@ -362,27 +387,4 @@ public void shouldSaveAndRetrieveIntegerFloats() { loadedAccount = session.load(Account.class, account.getId()); assertThat(loadedAccount.getLimit()).isEqualTo(account.getLimit()); } - - public void assertSameArray(Object[] as, Object[] bs) { - - if (as == null || bs == null) { - fail("null arrays not allowed"); - } - if (as.length != bs.length) { - fail("arrays are not same length"); - } - - for (Object a : as) { - boolean found = false; - for (Object b : bs) { - if (b.toString().equals(a.toString())) { - found = true; - break; - } - } - if (!found) { - fail("array contents are not the same: " + as + ", " + bs); - } - } - } }