Skip to content

Commit 80ddd91

Browse files
authored
Fixes #656 (Update Testcontainers + Jackson) (#667)
* Updates Testcontainers to v2 * Updates Jackson to v2.21.0 (to work with Docker Desktop) * Adds a new ZonedDateTime serializer and deserialzer to keep input/output consisten with xrpld’s handling of UTC timezone (xrpld uses `UTC`, newer Jackson versions nudge this to `Z`). * Update unit test coverage. * Misc import cleanup * CI failures are unrelated (reporting mode fails due to temDISABLED feature; likewise for Clio failures) Signed-off-by: David Fuelling <sappenin@gmail.com>
1 parent 7a53192 commit 80ddd91

File tree

26 files changed

+509
-140
lines changed

26 files changed

+509
-140
lines changed

pom.xml

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
<dependency>
118118
<groupId>org.junit</groupId>
119119
<artifactId>junit-bom</artifactId>
120-
<version>5.13.1</version>
120+
<version>5.14.1</version>
121121
<type>pom</type>
122122
<scope>import</scope>
123123
</dependency>
@@ -175,19 +175,14 @@
175175
<dependency>
176176
<groupId>org.awaitility</groupId>
177177
<artifactId>awaitility</artifactId>
178-
<version>4.2.0</version>
179-
<scope>test</scope>
180-
</dependency>
181-
<dependency>
182-
<groupId>org.awaitility</groupId>
183-
<artifactId>awaitility-proxy</artifactId>
184-
<version>3.1.6</version>
178+
<version>4.3.0</version>
185179
<scope>test</scope>
186180
</dependency>
187181
<dependency>
188182
<groupId>com.github.docker-java</groupId>
189183
<artifactId>docker-java-api</artifactId>
190-
<version>3.3.4</version>
184+
<version>3.7.0</version>
185+
<scope>test</scope>
191186
</dependency>
192187
<dependency>
193188
<groupId>org.hamcrest</groupId>
@@ -198,7 +193,7 @@
198193
<dependency>
199194
<groupId>org.testcontainers</groupId>
200195
<artifactId>testcontainers</artifactId>
201-
<version>1.20.2</version>
196+
<version>2.0.3</version>
202197
<scope>test</scope>
203198
</dependency>
204199
</dependencies>
@@ -279,7 +274,7 @@
279274
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
280275

281276
<cryptoconditions.version>1.0.5</cryptoconditions.version>
282-
<jackson.version>2.14.3</jackson.version>
277+
<jackson.version>2.21.0</jackson.version>
283278
<feign.version>12.3</feign.version>
284279
<slf4j.version>2.0.7</slf4j.version>
285280
<junit-jupiter.version>5.10.1</junit-jupiter.version>

xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService;
5151
import org.xrpl.xrpl4j.model.client.FinalityStatus;
5252
import org.xrpl.xrpl4j.model.client.XrplMethods;
53-
import org.xrpl.xrpl4j.model.client.XrplRequestParams;
5453
import org.xrpl.xrpl4j.model.client.XrplResult;
5554
import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsRequestParams;
5655
import org.xrpl.xrpl4j.model.client.accounts.AccountChannelsResult;
@@ -95,7 +94,6 @@
9594
import org.xrpl.xrpl4j.model.client.path.BookOffersResult;
9695
import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedRequestParams;
9796
import org.xrpl.xrpl4j.model.client.path.DepositAuthorizedResult;
98-
import org.xrpl.xrpl4j.model.client.path.ImmutableBookOffersRequestParams;
9997
import org.xrpl.xrpl4j.model.client.path.PathCurrency;
10098
import org.xrpl.xrpl4j.model.client.path.RipplePathFindRequestParams;
10199
import org.xrpl.xrpl4j.model.client.path.RipplePathFindResult;
@@ -128,7 +126,6 @@
128126
import java.time.ZonedDateTime;
129127
import java.util.Optional;
130128
import java.util.Set;
131-
import java.util.concurrent.TimeUnit;
132129
import java.util.concurrent.atomic.AtomicReference;
133130

134131
/**
@@ -1076,9 +1073,9 @@ void ledgerEntry() throws JsonRpcClientErrorException {
10761073
.method(XrplMethods.LEDGER_ENTRY)
10771074
.addParams(params)
10781075
.build(),
1079-
ObjectMapperFactory.create().getTypeFactory().constructParametricType(
1080-
LedgerEntryResult.class, LedgerObject.class
1081-
)
1076+
ObjectMapperFactory.create().getTypeFactory().constructParametricType(
1077+
LedgerEntryResult.class, LedgerObject.class
1078+
)
10821079
)).thenReturn(mockResult);
10831080

10841081
LedgerEntryResult<LedgerObject> result = xrplClient.ledgerEntry(params);

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/accounts/AccountTransactionsTransaction.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -33,8 +33,6 @@
3333
import org.xrpl.xrpl4j.model.transactions.Hash256;
3434
import org.xrpl.xrpl4j.model.transactions.Transaction;
3535

36-
import java.time.Instant;
37-
import java.time.ZoneId;
3836
import java.time.ZonedDateTime;
3937
import java.util.Optional;
4038

@@ -50,7 +48,7 @@
5048
using = AccountTransactionsTransactionDeserializer.class
5149
)
5250
public interface AccountTransactionsTransaction<T extends Transaction> {
53-
51+
5452
/**
5553
* Construct a builder for this class.
5654
*
@@ -86,17 +84,16 @@ static <T extends Transaction> ImmutableAccountTransactionsTransaction.Builder<T
8684
LedgerIndex ledgerIndex();
8785

8886
/**
89-
* The approximate close time (using Ripple Epoch) of the ledger containing this transaction.
90-
* This is an undocumented field.
87+
* The approximate close time (using Ripple Epoch) of the ledger containing this transaction. This is an undocumented
88+
* field.
9189
*
9290
* @return An optionally-present {@link UnsignedLong}.
9391
*/
9492
@JsonProperty("date")
9593
Optional<UnsignedLong> closeDate();
9694

9795
/**
98-
* The approximate close time in UTC offset.
99-
* This is derived from undocumented field.
96+
* The approximate close time in UTC offset. This is derived from undocumented field.
10097
*
10198
* @return An optionally-present {@link ZonedDateTime}.
10299
*/
@@ -105,5 +102,5 @@ static <T extends Transaction> ImmutableAccountTransactionsTransaction.Builder<T
105102
default Optional<ZonedDateTime> closeDateHuman() {
106103
return closeDate().map(TimeUtils::xrplTimeToZonedDateTime);
107104
}
108-
105+
109106
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/common/TimeUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.google.common.primitives.UnsignedLong;
44

55
import java.time.Instant;
6-
import java.time.ZoneId;
6+
import java.time.ZoneOffset;
77
import java.time.ZonedDateTime;
88

99
/**
@@ -25,7 +25,7 @@ public class TimeUtils {
2525
* @return A {@link ZonedDateTime} in UTC.
2626
*/
2727
public static ZonedDateTime xrplTimeToZonedDateTime(UnsignedLong xrplTime) {
28-
return Instant.ofEpochSecond(RIPPLE_EPOCH.plus(xrplTime).longValue()).atZone(ZoneId.of("UTC"));
28+
return Instant.ofEpochSecond(RIPPLE_EPOCH.plus(xrplTime).longValue()).atZone(ZoneOffset.UTC);
2929
}
3030

3131
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/client/transactions/TransactionResult.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -36,8 +36,6 @@
3636
import org.xrpl.xrpl4j.model.transactions.Transaction;
3737
import org.xrpl.xrpl4j.model.transactions.TransactionMetadata;
3838

39-
import java.time.Instant;
40-
import java.time.ZoneId;
4139
import java.time.ZonedDateTime;
4240
import java.util.Optional;
4341

@@ -82,6 +80,7 @@ static <T extends Transaction> ImmutableTransactionResult.Builder<T> builder() {
8280
* Get {@link #ledgerIndex()}, or throw an {@link IllegalStateException} if {@link #ledgerIndex()} is empty.
8381
*
8482
* @return The value of {@link #ledgerIndex()}.
83+
*
8584
* @throws IllegalStateException If {@link #ledgerIndex()} is empty.
8685
*/
8786
@JsonIgnore
@@ -118,17 +117,16 @@ default boolean validated() {
118117
Optional<TransactionMetadata> metadata();
119118

120119
/**
121-
* The approximate close time (using Ripple Epoch) of the ledger containing this transaction.
122-
* This is an undocumented field.
120+
* The approximate close time (using Ripple Epoch) of the ledger containing this transaction. This is an undocumented
121+
* field.
123122
*
124123
* @return An optionally-present {@link UnsignedLong}.
125124
*/
126125
@JsonProperty("date")
127126
Optional<UnsignedLong> closeDate();
128127

129128
/**
130-
* The approximate close time in UTC offset.
131-
* This is derived from undocumented field.
129+
* The approximate close time in UTC offset. This is derived from undocumented field.
132130
*
133131
* @return An optionally-present {@link ZonedDateTime}.
134132
*/

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/ObjectMapperFactory.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -49,13 +49,20 @@ public static ObjectMapper create() {
4949
return JsonMapper.builder()
5050
.addModule(new Jdk8Module())
5151
.addModule(new GuavaModule())
52-
.addModule(new Xrpl4jModule())
53-
.addModule(new JavaTimeModule())
5452
.addModule(new CryptoConditionsModule(Encoding.HEX))
53+
// Developer Note: The ordering here is important. The JavaTimeModule must be added before the Xrpl4jModule so
54+
// that the ZonedDateTimeSerializer is not overridden by the JavaTimeModule.
55+
.addModule(new JavaTimeModule())
56+
.addModule(new Xrpl4jModule())
5557
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
5658
.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
5759
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)
5860
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
61+
// Developer NOTE: Despite `serializationInclusion` being deprecated, we continue using it to for maximum
62+
// compatibility with software using xrpl4j that uses older versions of Jackson. While newer versions of
63+
// Jackson will define a more granular way to configure this behavior that we should actually employ at some point
64+
// (e.g., see https://github.com/XRPLF/xrpl4j/issues/671), we purposefully don't change this behavior for now
65+
// to ensure maximum compatibility.
5966
.serializationInclusion(JsonInclude.Include.NON_EMPTY)
6067
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
6168
.build();

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/jackson/modules/Xrpl4jModule.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* Licensed under the Apache License, Version 2.0 (the "License");
1010
* you may not use this file except in compliance with the License.
1111
* You may obtain a copy of the License at
12-
*
12+
*
1313
* http://www.apache.org/licenses/LICENSE-2.0
14-
*
14+
*
1515
* Unless required by applicable law or agreed to in writing, software
1616
* distributed under the License is distributed on an "AS IS" BASIS,
1717
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -24,13 +24,13 @@
2424
import com.fasterxml.jackson.databind.module.SimpleModule;
2525
import org.xrpl.xrpl4j.codec.addresses.UnsignedByteArray;
2626
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
27-
import org.xrpl.xrpl4j.model.client.path.BookOffersOffer;
2827
import org.xrpl.xrpl4j.model.client.serverinfo.ServerInfo;
2928
import org.xrpl.xrpl4j.model.flags.Flags;
3029
import org.xrpl.xrpl4j.model.transactions.CurrencyAmount;
3130
import org.xrpl.xrpl4j.model.transactions.Transaction;
3231
import org.xrpl.xrpl4j.model.transactions.metadata.AffectedNode;
33-
import org.xrpl.xrpl4j.model.transactions.metadata.MetaLedgerEntryType;
32+
33+
import java.time.ZonedDateTime;
3434

3535
/**
3636
* Jackson module for the xrpl4j-model project.
@@ -63,12 +63,15 @@ public Xrpl4jModule() {
6363
addDeserializer(Transaction.class, new TransactionDeserializer());
6464

6565
addDeserializer(ServerInfo.class, new ServerInfoDeserializer());
66-
66+
6767
addSerializer(UnsignedByteArray.class, new UnsignedByteArraySerializer());
6868
addDeserializer(UnsignedByteArray.class, new UnsignedByteArrayDeserializer());
6969

7070
addSerializer(Flags.class, new FlagsSerializer());
7171

7272
addDeserializer(AffectedNode.class, new AffectedNodeDeserializer());
73+
74+
addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer());
75+
addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer());
7376
}
7477
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.xrpl.xrpl4j.model.jackson.modules;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
import com.fasterxml.jackson.databind.BeanProperty;
6+
import com.fasterxml.jackson.databind.DeserializationContext;
7+
import com.fasterxml.jackson.databind.JsonDeserializer;
8+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
9+
10+
import java.io.IOException;
11+
import java.time.ZoneOffset;
12+
import java.time.ZonedDateTime;
13+
import java.time.format.DateTimeFormatter;
14+
import java.util.Locale;
15+
import java.util.Objects;
16+
17+
/**
18+
* A custom Jackson deserializer for {@link ZonedDateTime} usage in xrpl4j.
19+
*
20+
* <p>This deserializer is needed to allow newer versions of Jackson to deserialize ZonedDateTime values that conform
21+
* to the xrpld format, which uses `UTC` for all timezone designations. Newer versions of Jackson instead emit `Z`, so
22+
* this serializer ensures that Jackson's serialization for ZonedDateTime uses "UTC" instead of "Z".</p>
23+
*/
24+
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> implements ContextualDeserializer {
25+
26+
// Some XRPL dates have 9 digits of nanosecond precision (some have 6). This deserializer parses dates from various
27+
// sources that may have variable nanosecond precision, so using `[.SSSSSSSSS][.SSSSSS][.SSS]` handles 9, 6, or 3
28+
// decimal places correctly. For example, if this implementation merely used `[.n]`, n expects exactly 9 digits OR
29+
// interprets shorter values as literal nanoseconds. So, `.486384` (6 digits) → parsed as 486,384 nanoseconds, which
30+
// would be incorrect. Conversely, `.486384000` (9 digits) → parsed as 486,384,000 nanoseconds, which would be
31+
// correct. Therefore, we need to use multiple patterns to handle variable precision.
32+
private static final DateTimeFormatter DEFAULT_DESERIALIZATION_FORMATTER =
33+
DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss[.SSSSSSSSS][.SSSSSS][.SSS] z", Locale.US);
34+
35+
private final DateTimeFormatter formatter;
36+
37+
public ZonedDateTimeDeserializer() {
38+
this(DEFAULT_DESERIALIZATION_FORMATTER);
39+
}
40+
41+
private ZonedDateTimeDeserializer(final DateTimeFormatter formatter) {
42+
Objects.requireNonNull(formatter);
43+
this.formatter = formatter;
44+
}
45+
46+
@Override
47+
public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
48+
ZonedDateTime parsed = ZonedDateTime.parse(jsonParser.getText(), formatter);
49+
// Ensure the zone is explicitly UTC, not just Z
50+
return parsed.withZoneSameInstant(ZoneOffset.UTC);
51+
}
52+
53+
@Override
54+
public JsonDeserializer<?> createContextual(DeserializationContext context, BeanProperty property) {
55+
if (property != null) {
56+
JsonFormat.Value format = property.findPropertyFormat(context.getConfig(), ZonedDateTime.class);
57+
if (format != null && format.hasPattern()) {
58+
Locale locale = format.hasLocale() ? format.getLocale() : Locale.US;
59+
return new ZonedDateTimeDeserializer(DateTimeFormatter.ofPattern(format.getPattern(), locale));
60+
}
61+
}
62+
return this;
63+
}
64+
}

0 commit comments

Comments
 (0)