From 7e5d193d1b3922f93a27b47adc2003eb858fa457 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Thu, 15 Jun 2023 17:50:47 +0200 Subject: [PATCH] parse timestamps as ZonedDateTime internally Timestamps have been stored with their String representation from the API with convenience methods to convert them into ZonedDateTime. We now use the Jackson JavaTimeModule to parse them directly and swap the real and convenience getters. --- CHANGELOG.md | 6 +++ pom.xml | 8 ++- .../connector/internal/RequestHelper.java | 8 ++- .../model/response/SecretResponse.java | 9 +++- .../response/embedded/SecretMetadata.java | 49 +++++++++---------- .../model/response/embedded/TokenData.java | 38 ++++++++------ .../response/embedded/VersionMetadata.java | 49 +++++++++---------- .../model/response/embedded/WrapInfo.java | 7 +-- src/main/java/module-info.java | 1 + .../connector/model/AbstractModelTest.java | 8 ++- .../response/MetaSecretResponseTest.java | 2 +- 11 files changed, 108 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de32017..150eeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ ## unreleased +### Deprecations +* `get...TimeString()` methods on various model classes are now deprecated + ### Fix * Fixed JSON type conversion in `SecretResponse#get(String, Class)` (#67) +### Improvements +* Parse timestamps as `ZonedDateTime` instead of `String` representation + ## 1.1.4 (2023-06-15) diff --git a/pom.xml b/pom.xml index 458a141..e4fd371 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ de.stklcode.jvault jvault-connector - 1.1.5-SNAPSHOT + 1.2.0-SNAPSHOT jar @@ -51,6 +51,11 @@ jackson-databind 2.15.2 + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.15.2 + org.junit.jupiter @@ -170,6 +175,7 @@ --add-opens de.stklcode.jvault.connector/de.stklcode.jvault.connector.model.response=ALL-UNNAMED --add-opens de.stklcode.jvault.connector/de.stklcode.jvault.connector.model.response.embedded=ALL-UNNAMED --add-opens de.stklcode.jvault.connector/de.stklcode.jvault.connector.test=com.fasterxml.jackson.databind + --add-opens de.stklcode.jvault.connector/de.stklcode.jvault.connector.test=com.fasterxml.jackson.datatype.jsr310 diff --git a/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java b/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java index 724640f..ec047dc 100644 --- a/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java +++ b/src/main/java/de/stklcode/jvault/connector/internal/RequestHelper.java @@ -1,7 +1,10 @@ package de.stklcode.jvault.connector.internal; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.stklcode.jvault.connector.exception.*; import de.stklcode.jvault.connector.model.response.ErrorResponse; @@ -62,7 +65,10 @@ public RequestHelper(final String baseURL, this.timeout = timeout; this.tlsVersion = tlsVersion; this.trustedCaCert = trustedCaCert; - this.jsonMapper = new ObjectMapper(); + this.jsonMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); } /** diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/SecretResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/SecretResponse.java index c50a301..b256371 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/response/SecretResponse.java +++ b/src/main/java/de/stklcode/jvault/connector/model/response/SecretResponse.java @@ -17,7 +17,10 @@ package de.stklcode.jvault.connector.model.response; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.stklcode.jvault.connector.exception.InvalidResponseException; import de.stklcode.jvault.connector.model.response.embedded.VersionMetadata; @@ -82,7 +85,11 @@ public final C get(final String key, final Class type) throws InvalidResp } else if (type.isInstance(rawValue)) { return type.cast(rawValue); } else { - var om = new ObjectMapper(); + var om = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + if (rawValue instanceof String) { return om.readValue((String) rawValue, type); } else { diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/SecretMetadata.java b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/SecretMetadata.java index 2773bcb..503aaad 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/SecretMetadata.java +++ b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/SecretMetadata.java @@ -22,7 +22,6 @@ import java.io.Serializable; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.util.Map; import java.util.Objects; @@ -35,13 +34,13 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) public final class SecretMetadata implements Serializable { - private static final long serialVersionUID = 1684891108903409038L; + private static final long serialVersionUID = -4967896264361344676L; private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSXXX"); @JsonProperty("created_time") - private String createdTimeString; + private ZonedDateTime createdTime; @JsonProperty("current_version") private Integer currentVersion; @@ -53,31 +52,29 @@ public final class SecretMetadata implements Serializable { private Integer oldestVersion; @JsonProperty("updated_time") - private String updatedTime; + private ZonedDateTime updatedTime; @JsonProperty("versions") private Map versions; /** * @return Time of secret creation as raw string representation. + * @deprecated Method left for backwards compatibility only. Use {@link #getCreatedTime()} instead. */ + @Deprecated(since = "1.2", forRemoval = true) public String getCreatedTimeString() { - return createdTimeString; + if (createdTime != null) { + return TIME_FORMAT.format(createdTime); + } + + return null; } /** * @return Time of secret creation. */ public ZonedDateTime getCreatedTime() { - if (createdTimeString != null && !createdTimeString.isEmpty()) { - try { - return ZonedDateTime.parse(createdTimeString, TIME_FORMAT); - } catch (DateTimeParseException e) { - // Ignore. - } - } - - return null; + return createdTime; } /** @@ -103,24 +100,22 @@ public Integer getOldestVersion() { /** * @return Time of secret update as raw string representation. + * @deprecated Method left for backwards compatibility only. Use {@link #getUpdatedTime()} instead. */ + @Deprecated(since = "1.2", forRemoval = true) public String getUpdatedTimeString() { - return updatedTime; + if (updatedTime != null) { + return TIME_FORMAT.format(updatedTime); + } + + return null; } /** - * @return Time of secret update.. + * @return Time of secret update. */ public ZonedDateTime getUpdatedTime() { - if (updatedTime != null && !updatedTime.isEmpty()) { - try { - return ZonedDateTime.parse(updatedTime, TIME_FORMAT); - } catch (DateTimeParseException e) { - // Ignore. - } - } - - return null; + return updatedTime; } /** @@ -138,7 +133,7 @@ public boolean equals(Object o) { return false; } SecretMetadata that = (SecretMetadata) o; - return Objects.equals(createdTimeString, that.createdTimeString) && + return Objects.equals(createdTime, that.createdTime) && Objects.equals(currentVersion, that.currentVersion) && Objects.equals(maxVersions, that.maxVersions) && Objects.equals(oldestVersion, that.oldestVersion) && @@ -148,6 +143,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(createdTimeString, currentVersion, maxVersions, oldestVersion, updatedTime, versions); + return Objects.hash(createdTime, currentVersion, maxVersions, oldestVersion, updatedTime, versions); } } diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/TokenData.java b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/TokenData.java index 39d6de1..a7d5fda 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/TokenData.java +++ b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/TokenData.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.Objects; @@ -34,7 +35,10 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) public final class TokenData implements Serializable { - private static final long serialVersionUID = 2915180734313753649L; + private static final long serialVersionUID = -5749716740973138916L; + + private static final DateTimeFormatter TIME_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSXXX"); @JsonProperty("accessor") private String accessor; @@ -52,7 +56,7 @@ public final class TokenData implements Serializable { private String entityId; @JsonProperty("expire_time") - private String expireTime; + private ZonedDateTime expireTime; @JsonProperty("explicit_max_ttl") private Integer explicitMaxTtl; @@ -61,7 +65,7 @@ public final class TokenData implements Serializable { private String id; @JsonProperty("issue_time") - private String issueTime; + private ZonedDateTime issueTime; @JsonProperty("meta") private Map meta; @@ -126,9 +130,15 @@ public String getEntityId() { /** * @return Expire time as raw string value * @since 0.9 + * @deprecated Method left for backwards compatibility only. Use {@link #getExpireTime()} instead. */ + @Deprecated(since = "1.2", forRemoval = true) public String getExpireTimeString() { - return expireTime; + if (expireTime != null) { + return TIME_FORMAT.format(expireTime); + } + + return null; } /** @@ -136,11 +146,7 @@ public String getExpireTimeString() { * @since 0.9 */ public ZonedDateTime getExpireTime() { - if (expireTime == null) { - return null; - } else { - return ZonedDateTime.parse(expireTime); - } + return expireTime; } /** @@ -161,9 +167,15 @@ public String getId() { /** * @return Issue time as raw string value * @since 0.9 + * @deprecated Method left for backwards compatibility only. Use {@link #getIssueTime()} instead. */ + @Deprecated(since = "1.2", forRemoval = true) public String getIssueTimeString() { - return issueTime; + if (issueTime != null) { + return TIME_FORMAT.format(issueTime); + } + + return null; } /** @@ -171,11 +183,7 @@ public String getIssueTimeString() { * @since 0.9 */ public ZonedDateTime getIssueTime() { - if (issueTime == null) { - return null; - } else { - return ZonedDateTime.parse(issueTime); - } + return issueTime; } /** diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/VersionMetadata.java b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/VersionMetadata.java index be9f3f9..5f1d3d1 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/VersionMetadata.java +++ b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/VersionMetadata.java @@ -22,7 +22,6 @@ import java.io.Serializable; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.util.Objects; /** @@ -34,16 +33,16 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) public final class VersionMetadata implements Serializable { - private static final long serialVersionUID = -5286693953873839611L; + private static final long serialVersionUID = -6815731513868586713L; private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSXXX"); @JsonProperty("created_time") - private String createdTimeString; + private ZonedDateTime createdTime; @JsonProperty("deletion_time") - private String deletionTimeString; + private ZonedDateTime deletionTime; @JsonProperty("destroyed") private boolean destroyed; @@ -53,46 +52,42 @@ public final class VersionMetadata implements Serializable { /** * @return Time of secret creation as raw string representation. + * @deprecated Method left for backwards compatibility only. Use {@link #getCreatedTime()} instead. */ + @Deprecated(since = "1.2", forRemoval = true) public String getCreatedTimeString() { - return createdTimeString; + if (createdTime != null) { + return TIME_FORMAT.format(createdTime); + } + + return null; } /** * @return Time of secret creation. */ public ZonedDateTime getCreatedTime() { - if (createdTimeString != null && !createdTimeString.isEmpty()) { - try { - return ZonedDateTime.parse(createdTimeString, TIME_FORMAT); - } catch (DateTimeParseException e) { - // Ignore. - } - } - - return null; + return createdTime; } /** * @return Time for secret deletion as raw string representation. + * @deprecated Method left for backwards compatibility only. Use {@link #getDeletionTime()} instead. */ + @Deprecated(since = "1.2", forRemoval = true) public String getDeletionTimeString() { - return deletionTimeString; + if (deletionTime != null) { + return TIME_FORMAT.format(deletionTime); + } + + return null; } /** * @return Time for secret deletion. */ public ZonedDateTime getDeletionTime() { - if (deletionTimeString != null && !deletionTimeString.isEmpty()) { - try { - return ZonedDateTime.parse(deletionTimeString, TIME_FORMAT); - } catch (DateTimeParseException e) { - // Ignore. - } - } - - return null; + return deletionTime; } /** @@ -118,13 +113,13 @@ public boolean equals(Object o) { } VersionMetadata that = (VersionMetadata) o; return destroyed == that.destroyed && - Objects.equals(createdTimeString, that.createdTimeString) && - Objects.equals(deletionTimeString, that.deletionTimeString) && + Objects.equals(createdTime, that.createdTime) && + Objects.equals(deletionTime, that.deletionTime) && Objects.equals(version, that.version); } @Override public int hashCode() { - return Objects.hash(createdTimeString, deletionTimeString, destroyed, version); + return Objects.hash(createdTime, deletionTime, destroyed, version); } } diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/WrapInfo.java b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/WrapInfo.java index d89eee4..64ebeb5 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/response/embedded/WrapInfo.java +++ b/src/main/java/de/stklcode/jvault/connector/model/response/embedded/WrapInfo.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; +import java.time.ZonedDateTime; import java.util.Objects; /** @@ -28,7 +29,7 @@ * @since 1.1 */ public class WrapInfo implements Serializable { - private static final long serialVersionUID = -7764500642913116581L; + private static final long serialVersionUID = 4864973237090355607L; @JsonProperty("token") private String token; @@ -37,7 +38,7 @@ public class WrapInfo implements Serializable { private Integer ttl; @JsonProperty("creation_time") - private String creationTime; + private ZonedDateTime creationTime; @JsonProperty("creation_path") private String creationPath; @@ -59,7 +60,7 @@ public Integer getTtl() { /** * @return Creation time */ - public String getCreationTime() { + public ZonedDateTime getCreationTime() { return creationTime; } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 1067c2a..42648bc 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -33,4 +33,5 @@ requires java.base; requires java.net.http; requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jsr310; } diff --git a/src/test/java/de/stklcode/jvault/connector/model/AbstractModelTest.java b/src/test/java/de/stklcode/jvault/connector/model/AbstractModelTest.java index 0ea2bed..2974339 100644 --- a/src/test/java/de/stklcode/jvault/connector/model/AbstractModelTest.java +++ b/src/test/java/de/stklcode/jvault/connector/model/AbstractModelTest.java @@ -1,6 +1,9 @@ package de.stklcode.jvault.connector.model; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import nl.jqno.equalsverifier.EqualsVerifier; import org.junit.jupiter.api.Test; @@ -26,7 +29,10 @@ public abstract class AbstractModelTest { */ protected AbstractModelTest(Class modelClass) { this.modelClass = modelClass; - this.objectMapper = new ObjectMapper(); + this.objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); } /** diff --git a/src/test/java/de/stklcode/jvault/connector/model/response/MetaSecretResponseTest.java b/src/test/java/de/stklcode/jvault/connector/model/response/MetaSecretResponseTest.java index bdc360e..42ffffc 100644 --- a/src/test/java/de/stklcode/jvault/connector/model/response/MetaSecretResponseTest.java +++ b/src/test/java/de/stklcode/jvault/connector/model/response/MetaSecretResponseTest.java @@ -109,7 +109,7 @@ void jsonRoundtrip() { assertNotNull(res.getMetadata(), "SecretResponse does not contain metadata"); assertEquals(SECRET_META_CREATED, res.getMetadata().getCreatedTimeString(), "Incorrect creation date string"); assertNotNull(res.getMetadata().getCreatedTime(), "Creation date parsing failed"); - assertEquals("", res.getMetadata().getDeletionTimeString(), "Incorrect deletion date string"); + assertNull(res.getMetadata().getDeletionTimeString(), "Incorrect deletion date string"); assertNull(res.getMetadata().getDeletionTime(), "Incorrect deletion date"); assertFalse(res.getMetadata().isDestroyed(), "Secret destroyed when not expected"); assertEquals(1, res.getMetadata().getVersion(), "Incorrect secret version");