From 0ed70ebe851c78eb65d66f735219bec33d5531d9 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 12 Mar 2026 13:36:00 +0100 Subject: [PATCH 1/4] Upgrade dependencies --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index b60f67d..40e5925 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ com.typesafe config - 1.4.5 + 1.4.6 org.slf4j @@ -121,7 +121,7 @@ nl.jqno.equalsverifier equalsverifier - 4.2.5 + 4.4.1 test @@ -149,14 +149,14 @@ io.dropwizard dropwizard-bom - 5.0.0 + 5.0.1 pom import io.dropwizard dropwizard-dependencies - 5.0.0 + 5.0.1 pom import From 76eff1e8cb3c683a361b205bc0de1cf7c0b0a730 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 12 Mar 2026 16:52:23 +0100 Subject: [PATCH 2/4] Upgrade Maven plugins --- pom.xml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 40e5925..481d452 100644 --- a/pom.xml +++ b/pom.xml @@ -172,19 +172,19 @@ maven-resources-plugin - 3.4.0 + 3.5.0 maven-compiler-plugin - 3.14.1 + 3.15.0 maven-surefire-plugin - 3.5.4 + 3.5.5 maven-jar-plugin - 3.4.2 + 3.5.0 maven-install-plugin @@ -197,16 +197,21 @@ org.codehaus.mojo versions-maven-plugin - 2.20.1 + 2.21.0 maven-dependency-plugin - 3.9.0 + 3.10.0 maven-enforcer-plugin 3.6.2 + + com.github.siom79.japicmp + japicmp-maven-plugin + 0.25.4 + From 187c5d24158782f3d5a14b18715b49aacd20e1f9 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 12 Mar 2026 16:37:38 +0100 Subject: [PATCH 3/4] Support create JsonDuration from Duration To e.g. enable initializing JsonDuration fields from existing constants in your application domain, which will typically be of type Duration. In addition, use a more descriptive name for the method used to parse String values on the form "1 hours", "42 seconds", etc. Deprecating JsonDuration.of(String). --- .../no/digipost/jackson/JsonDuration.java | 52 ++++++++++++--- .../no/digipost/jackson/JsonDurationTest.java | 66 +++++++++++++++---- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/src/main/java/no/digipost/jackson/JsonDuration.java b/src/main/java/no/digipost/jackson/JsonDuration.java index 84a28fb..f01a338 100644 --- a/src/main/java/no/digipost/jackson/JsonDuration.java +++ b/src/main/java/no/digipost/jackson/JsonDuration.java @@ -29,6 +29,12 @@ import java.util.stream.Stream; import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MICROS; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.NANOS; +import static java.time.temporal.ChronoUnit.SECONDS; import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -42,22 +48,51 @@ public final class JsonDuration implements TemporalAmount, Serializable { public final Duration duration; private final String stringRepresentation; - @JsonCreator + @Deprecated public static JsonDuration of(String jsonString) { - return new JsonDuration(jsonString); + return parse(jsonString); } - private JsonDuration(String jsonString) { + @JsonCreator + public static JsonDuration parse(String jsonString) { try { String[] amountAndUnit = jsonString.split("\\s+"); - amount = Long.parseLong(amountAndUnit[0]); - ChronoUnit chronoUnit = ChronoUnit.valueOf(amountAndUnit[1].toUpperCase()); - unit = chronoUnit; - duration = Duration.of(amount, unit); - stringRepresentation = amount + " " + chronoUnit.name(); + long amount = Long.parseLong(amountAndUnit[0]); + ChronoUnit unit = ChronoUnit.valueOf(amountAndUnit[1].toUpperCase()); + return new JsonDuration(amount, unit, null); } catch (Exception e) { throw new CannotConvertToJsonDuration(jsonString, e); } + + } + + public static JsonDuration from(Duration duration) { + long nanosPart = duration.toNanosPart(); + if (nanosPart != 0) { + long totalNanos = duration.toNanos(); + if (totalNanos % 1000 != 0) { + return new JsonDuration(totalNanos, NANOS, duration); + } else if (totalNanos % 1_000_000 != 0) { + return new JsonDuration(totalNanos / 1000, MICROS, duration); + } else { + return new JsonDuration(duration.toMillis(), MILLIS, duration); + } + } else if (duration.toSecondsPart() != 0) { + return new JsonDuration(duration.toSeconds(), SECONDS, duration); + } else if (duration.toMinutesPart() != 0) { + return new JsonDuration(duration.toMinutes(), MINUTES, duration); + } else if (duration.toHoursPart() != 0) { + return new JsonDuration(duration.toHours(), HOURS, duration); + } else { + return new JsonDuration(duration.toDays(), DAYS, duration); + } + } + + private JsonDuration(long amount, ChronoUnit unit, Duration duration) { + this.amount = amount; + this.unit = unit; + this.stringRepresentation = amount + " " + unit.name(); + this.duration = duration != null ? duration : Duration.of(amount, unit); } @@ -110,4 +145,5 @@ public boolean equals(Object obj) { public int hashCode() { return Objects.hash(duration); } + } diff --git a/src/test/java/no/digipost/jackson/JsonDurationTest.java b/src/test/java/no/digipost/jackson/JsonDurationTest.java index 766d83c..24968c2 100644 --- a/src/test/java/no/digipost/jackson/JsonDurationTest.java +++ b/src/test/java/no/digipost/jackson/JsonDurationTest.java @@ -16,57 +16,97 @@ package no.digipost.jackson; import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.time.Duration; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MICROS; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.NANOS; +import static java.time.temporal.ChronoUnit.SECONDS; import static no.digipost.jackson.JsonDuration.supportedUnits; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.quicktheories.QuickTheory.qt; import static org.quicktheories.generators.SourceDSL.arbitrary; import static org.quicktheories.generators.SourceDSL.integers; +import static org.quicktheories.generators.SourceDSL.longs; import static org.quicktheories.generators.SourceDSL.strings; +import static uk.co.probablyfine.matchers.Java8Matchers.where; -public class JsonDurationTest { +class JsonDurationTest { @Test - public void correctEqualsAndHashcode() { + void correctEqualsAndHashcode() { EqualsVerifier - .forRelaxedEqualExamples(JsonDuration.of("1 days"), JsonDuration.of("24 hours"), JsonDuration.of("1440 minutes")) - .andUnequalExamples(JsonDuration.of("4 days"), JsonDuration.of("5 days"), JsonDuration.of("1337 nanos")) + .forRelaxedEqualExamples(JsonDuration.parse("1 days"), JsonDuration.parse("24 hours"), JsonDuration.parse("1440 minutes")) + .andUnequalExamples(JsonDuration.parse("4 days"), JsonDuration.parse("5 days"), JsonDuration.parse("1337 nanos")) .verify(); - assertThat(JsonDuration.of("1 days"), is(JsonDuration.of("24 hours"))); - assertThat(JsonDuration.of("42 minutes"), not("42 minutes")); + assertThat(JsonDuration.parse("1 days"), is(JsonDuration.parse("24 hours"))); + assertThat(JsonDuration.parse("42 minutes"), not("42 minutes")); } @Test - public void parsesUnitsSupportedByJavaTimeDuration() { + void parsesUnitsSupportedByJavaTimeDuration() { qt() .forAll(integers().all(), arbitrary().pick(supportedUnits)) .checkAssert((amount, unit) -> { - JsonDuration parsed = JsonDuration.of(amount + " " + unit.name().toLowerCase()); + JsonDuration parsed = JsonDuration.parse(amount + " " + unit.name().toLowerCase()); assertThat(parsed.duration.toMillis(), is(Duration.of(amount, unit).toMillis())); }); } @Test - public void stringRepresentationOfItselfIsParsable() { + void stringRepresentationOfItselfIsParsable() { qt() .forAll(integers().all(), arbitrary().pick(supportedUnits)) - .as((amount, unit) -> JsonDuration.of(amount + " " + unit.name())) - .checkAssert(parsed -> assertThat(JsonDuration.of(parsed.toString()), is(parsed))); + .as((amount, unit) -> JsonDuration.parse(amount + " " + unit.name())) + .checkAssert(parsed -> assertThat(JsonDuration.parse(parsed.toString()), is(parsed))); } @Test - public void unableToParseMalforedStrings() { + void unableToParseMalforedStrings() { qt() .forAll(strings().allPossible().ofLengthBetween(0, 100).assuming(s -> !s.matches("\\d+ \\w\\w+"))) - .checkAssert(notParseable -> assertThrows(JsonDuration.CannotConvertToJsonDuration.class, () -> JsonDuration.of(notParseable))); + .checkAssert(notParseable -> assertThrows(JsonDuration.CannotConvertToJsonDuration.class, () -> JsonDuration.parse(notParseable))); } + + @Nested + class ConvertFromDuration { + @Test + void resolvedAmountAndUnitIsEquivalentToTheDuration() { + qt() + .forAll(longs().all().map(Duration::ofNanos).map(JsonDuration::from)) + .checkAssert(jsonDuration -> assertThat(Duration.of(jsonDuration.amount, jsonDuration.unit), is(jsonDuration.duration))); + } + + @Test + void useASensibleLargestPossibleUnit() { + assertAll( + () -> assertThat(JsonDuration.from(Duration.ofHours(24)), where(d -> d.unit, is(DAYS))), + () -> assertThat(JsonDuration.from(Duration.ofHours(25)), where(d -> d.unit, is(HOURS))), + () -> assertThat(JsonDuration.from(Duration.ofHours(25).plusSeconds(42)), where(d -> d.unit, is(SECONDS))), + () -> assertThat(JsonDuration.from(Duration.ofHours(24).plusMinutes(10)), where(d -> d.unit, is(MINUTES)))); + } + + @Test + void handlesSubSecondUnits() { + assertAll( + () -> assertThat(JsonDuration.from(Duration.ofNanos(1_000_000_000)), where(d -> d.unit, is(SECONDS))), + () -> assertThat(JsonDuration.from(Duration.ofNanos(1_001_000_000)), where(d -> d.unit, is(MILLIS))), + () -> assertThat(JsonDuration.from(Duration.ofNanos(1_001_001_000)), where(d -> d.unit, is(MICROS))), + () -> assertThat(JsonDuration.from(Duration.ofNanos(1_001_001_001)), where(d -> d.unit, is(NANOS)))); + } + } + } From cc7cc3ea9a1897341bfe339dcb810cd9c6fd1248 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 12 Mar 2026 16:52:37 +0100 Subject: [PATCH 4/4] Add previously autogenerated serialVersionUID To enforce backwards compatibility. Internal representation has not changed in JsonDuration. --- src/main/java/no/digipost/jackson/JsonDuration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/no/digipost/jackson/JsonDuration.java b/src/main/java/no/digipost/jackson/JsonDuration.java index f01a338..9097260 100644 --- a/src/main/java/no/digipost/jackson/JsonDuration.java +++ b/src/main/java/no/digipost/jackson/JsonDuration.java @@ -41,6 +41,8 @@ public final class JsonDuration implements TemporalAmount, Serializable { + private static final long serialVersionUID = 7565437081331942214L; + public static final List supportedUnits = unmodifiableList(Stream.of(ChronoUnit.values()).filter(u -> !u.isDurationEstimated() || u == DAYS).collect(toList())); public final long amount;